LambdaHack 0.2.10.6 → 0.2.12
raw patch · 123 files changed
+8270/−6320 lines, 123 filesdep +hsinidep +vectordep +vector-binary-instancesdep −ConfigFiledep ~hashabledep ~pretty-showdep ~vtybinary-added
Dependencies added: hsini, vector, vector-binary-instances
Dependencies removed: ConfigFile
Dependency ranges changed: hashable, pretty-show, vty
Files
- .travis.yml +0/−10
- Game/LambdaHack/Client.hs +19/−18
- Game/LambdaHack/Client/Action.hs +599/−179
- Game/LambdaHack/Client/Action/ActionClass.hs +0/−26
- Game/LambdaHack/Client/Action/ActionType.hs +50/−91
- Game/LambdaHack/Client/AtomicSemCli.hs +183/−89
- Game/LambdaHack/Client/Binding.hs +58/−64
- Game/LambdaHack/Client/ClientSem.hs +254/−184
- Game/LambdaHack/Client/Config.hs +10/−36
- Game/LambdaHack/Client/Draw.hs +218/−133
- Game/LambdaHack/Client/HumanCmd.hs +0/−152
- Game/LambdaHack/Client/HumanGlobal.hs +371/−232
- Game/LambdaHack/Client/HumanLocal.hs +573/−423
- Game/LambdaHack/Client/HumanSem.hs +60/−118
- Game/LambdaHack/Client/LoopAction.hs +10/−9
- Game/LambdaHack/Client/RunAction.hs +242/−177
- Game/LambdaHack/Client/State.hs +113/−40
- Game/LambdaHack/Client/Strategy.hs +8/−6
- Game/LambdaHack/Client/StrategyAction.hs +515/−373
- Game/LambdaHack/Common/Ability.hs +2/−1
- Game/LambdaHack/Common/Action.hs +1/−10
- Game/LambdaHack/Common/Actor.hs +42/−38
- Game/LambdaHack/Common/ActorState.hs +64/−34
- Game/LambdaHack/Common/Animation.hs +102/−73
- Game/LambdaHack/Common/AtomicCmd.hs +4/−4
- Game/LambdaHack/Common/AtomicPos.hs +43/−41
- Game/LambdaHack/Common/AtomicSem.hs +37/−40
- Game/LambdaHack/Common/ClientCmd.hs +12/−12
- Game/LambdaHack/Common/Color.hs +32/−21
- Game/LambdaHack/Common/ConfigIO.hs +0/−97
- Game/LambdaHack/Common/Effect.hs +18/−18
- Game/LambdaHack/Common/Faction.hs +17/−7
- Game/LambdaHack/Common/Feature.hs +5/−7
- Game/LambdaHack/Common/Flavour.hs +2/−3
- Game/LambdaHack/Common/HighScore.hs +51/−44
- Game/LambdaHack/Common/HumanCmd.hs +158/−0
- Game/LambdaHack/Common/Item.hs +47/−10
- Game/LambdaHack/Common/ItemFeature.hs +23/−0
- Game/LambdaHack/Common/Key.hs +45/−82
- Game/LambdaHack/Common/Kind.hs +40/−76
- Game/LambdaHack/Common/Level.hs +55/−27
- Game/LambdaHack/Common/Misc.hs +4/−8
- Game/LambdaHack/Common/Msg.hs +59/−62
- Game/LambdaHack/Common/Perception.hs +20/−59
- Game/LambdaHack/Common/Point.hs +109/−86
- Game/LambdaHack/Common/PointArray.hs +133/−0
- Game/LambdaHack/Common/PointXY.hs +0/−79
- Game/LambdaHack/Common/Random.hs +14/−13
- Game/LambdaHack/Common/Save.hs +10/−8
- Game/LambdaHack/Common/ServerCmd.hs +25/−17
- Game/LambdaHack/Common/State.hs +21/−18
- Game/LambdaHack/Common/Tile.hs +101/−40
- Game/LambdaHack/Common/Time.hs +13/−14
- Game/LambdaHack/Common/Vector.hs +285/−108
- Game/LambdaHack/Common/VectorXY.hs +0/−57
- Game/LambdaHack/Content/ActorKind.hs +7/−4
- Game/LambdaHack/Content/CaveKind.hs +19/−16
- Game/LambdaHack/Content/FactionKind.hs +4/−4
- Game/LambdaHack/Content/ItemKind.hs +15/−14
- Game/LambdaHack/Content/ModeKind.hs +9/−5
- Game/LambdaHack/Content/PlaceKind.hs +4/−5
- Game/LambdaHack/Content/RuleKind.hs +63/−20
- Game/LambdaHack/Content/TileKind.hs +54/−26
- Game/LambdaHack/Frontend.hs +11/−9
- Game/LambdaHack/Frontend/Chosen.hs +9/−1
- Game/LambdaHack/Frontend/Curses.hs +15/−12
- Game/LambdaHack/Frontend/Gtk.hs +28/−38
- Game/LambdaHack/Frontend/Std.hs +5/−7
- Game/LambdaHack/Frontend/Vty.hs +17/−15
- Game/LambdaHack/Server.hs +51/−33
- Game/LambdaHack/Server/Action.hs +127/−123
- Game/LambdaHack/Server/Action/ActionType.hs +15/−13
- Game/LambdaHack/Server/AtomicSemSer.hs +5/−8
- Game/LambdaHack/Server/Config.hs +0/−53
- Game/LambdaHack/Server/DungeonGen.hs +28/−27
- Game/LambdaHack/Server/DungeonGen/Area.hs +8/−8
- Game/LambdaHack/Server/DungeonGen/AreaRnd.hs +44/−39
- Game/LambdaHack/Server/DungeonGen/Cave.hs +37/−45
- Game/LambdaHack/Server/DungeonGen/Place.hs +64/−44
- Game/LambdaHack/Server/EffectSem.hs +112/−76
- Game/LambdaHack/Server/Fov.hs +55/−85
- Game/LambdaHack/Server/Fov/Common.hs +15/−9
- Game/LambdaHack/Server/Fov/Digital.hs +50/−39
- Game/LambdaHack/Server/Fov/Permissive.hs +34/−30
- Game/LambdaHack/Server/Fov/Shadow.hs +1/−1
- Game/LambdaHack/Server/LoopAction.hs +231/−101
- Game/LambdaHack/Server/ServerSem.hs +169/−110
- Game/LambdaHack/Server/StartAction.hs +50/−24
- Game/LambdaHack/Server/State.hs +78/−26
- Game/LambdaHack/Utils/File.hs +26/−6
- Game/LambdaHack/Utils/Frequency.hs +7/−6
- GameDefinition/Content/ActorKind.hs +88/−0
- GameDefinition/Content/CaveKind.hs +123/−0
- GameDefinition/Content/FactionKind.hs +55/−0
- GameDefinition/Content/ItemKind.hs +267/−0
- GameDefinition/Content/ModeKind.hs +301/−0
- GameDefinition/Content/PlaceKind.hs +71/−0
- GameDefinition/Content/RuleKind.hs +213/−0
- GameDefinition/Content/TileKind.hs +260/−0
- GameDefinition/Main.hs +50/−0
- GameDefinition/MainMenu.ascii +26/−0
- GameDefinition/PLAYING.md +204/−0
- GameDefinition/config.ui.default +26/−0
- GameDefinition/scores binary
- LICENSE +2/−2
- LambdaHack.cabal +27/−32
- LambdaHack/Content/ActorKind.hs +0/−88
- LambdaHack/Content/CaveKind.hs +0/−116
- LambdaHack/Content/FactionKind.hs +0/−55
- LambdaHack/Content/ItemKind.hs +0/−206
- LambdaHack/Content/ModeKind.hs +0/−259
- LambdaHack/Content/PlaceKind.hs +0/−71
- LambdaHack/Content/RuleKind.hs +0/−82
- LambdaHack/Content/TileKind.hs +0/−307
- LambdaHack/Main.hs +0/−50
- MainMenu.ascii +0/−26
- Makefile +149/−55
- PLAYING.md +0/−171
- README.md +22/−22
- changelog +42/−31
- config.rules.default +0/−28
- config.ui.default +0/−63
- scores binary
− .travis.yml
@@ -1,10 +0,0 @@-language: haskell--install:- - cabal install gtk2hs-buildtools- - cabal install -f-vty -f-curses --only-dependencies- - cabal install -fvty -f-curses --only-dependencies- - cabal install -f-vty -fcurses --only-dependencies--script:- - cabal install -f-vty -f-curses && (make test-travis || (cat ~/.LambdaHack/config.rules.dump ; tail -n 200 /tmp/stdtest.log ; exit 77)) && cabal install -fvty -f-curses && (make test-travis || (cat ~/.LambdaHack/config.rules.dump ; tail -n 200 /tmp/stdtest.log ; exit 77)) && cabal install -f-vty -fcurses && (make test-travis || (cat ~/.LambdaHack/config.rules.dump ; tail -n 200 /tmp/stdtest.log ; exit 77))
Game/LambdaHack/Client.hs view
@@ -3,11 +3,11 @@ -- See -- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>. module Game.LambdaHack.Client- ( cmdClientAISem, cmdClientUISem- , loopAI, loopUI, exeFrontend+ ( cmdClientAISem, cmdClientUISem, exeFrontend , MonadClient, MonadClientUI, MonadClientReadServer, MonadClientWriteServer ) where +import Control.Exception.Assert.Sugar import Control.Monad import Data.Maybe @@ -27,21 +27,21 @@ import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.ServerCmd import Game.LambdaHack.Common.State+import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Frontend-import Control.Exception.Assert.Sugar storeUndo :: MonadClient m => Atomic -> m () storeUndo _atomic = maybe skip (\a -> modifyClient $ \cli -> cli {sundo = a : sundo cli}) $ Nothing -- TODO: undoAtomic atomic -cmdClientAISem :: (MonadAtomic m, MonadClientWriteServer CmdSerTakeTime m)+cmdClientAISem :: (MonadAtomic m, MonadClientWriteServer CmdTakeTimeSer m) => CmdClientAI -> m () cmdClientAISem cmd = case cmd of CmdAtomicAI cmdA -> do cmds <- cmdAtomicFilterCli cmdA- mapM_ cmdAtomicSemCli cmds- mapM_ execCmdAtomic cmds+ mapM_ (\c -> cmdAtomicSemCli c+ >> execCmdAtomic c) cmds mapM_ (storeUndo . CmdAtomic) cmds CmdQueryAI aid -> do cmdC <- queryAI aid@@ -49,15 +49,15 @@ CmdPingAI -> writeServer $ WaitSer $ toEnum (-1) -cmdClientUISem :: ( MonadAtomic m, MonadClientAbort m- , MonadClientUI m, MonadClientWriteServer CmdSer m )+cmdClientUISem :: ( MonadAtomic m, MonadClientUI m+ , MonadClientWriteServer CmdSer m ) => CmdClientUI -> m () cmdClientUISem cmd = case cmd of CmdAtomicUI cmdA -> do cmds <- cmdAtomicFilterCli cmdA- mapM_ cmdAtomicSemCli cmds- mapM_ execCmdAtomic cmds- mapM_ (drawCmdAtomicUI False) cmds+ mapM_ (\c -> cmdAtomicSemCli c+ >> execCmdAtomic c+ >> drawCmdAtomicUI False c) cmds mapM_ (storeUndo . CmdAtomic) cmds -- TODO: only store cmdA? SfxAtomicUI sfx -> do drawSfxAtomicUI False sfx@@ -72,28 +72,28 @@ snoMore <- getsClient $ snoMore . sdebugCli when snoMore $ void $ displayMore ColorFull "Flushing frames." -- Return the ping.- writeServer $ TakeTimeSer $ WaitSer $ toEnum (-1)+ writeServer $ CmdTakeTimeSer $ WaitSer $ toEnum (-1) -- | Wire together game content, the main loop of game clients, -- the main game loop assigned to this frontend (possibly containing -- the server loop, if the whole game runs in one process), -- UI config and the definitions of game commands.-exeFrontend :: ( MonadAtomic m, MonadClientAbort m, MonadClientUI m+exeFrontend :: ( MonadAtomic m, MonadClientUI m , MonadClientReadServer CmdClientUI m , MonadClientWriteServer CmdSer m , MonadAtomic n , MonadClientReadServer CmdClientAI n- , MonadClientWriteServer CmdSerTakeTime n )+ , MonadClientWriteServer CmdTakeTimeSer n ) => (m () -> SessionUI -> State -> StateClient -> ChanServer CmdClientUI CmdSer -> IO ()) -> (n () -> SessionUI -> State -> StateClient- -> ChanServer CmdClientAI CmdSerTakeTime+ -> ChanServer CmdClientAI CmdTakeTimeSer -> IO ()) -> Kind.COps -> DebugModeCli -> ((FactionId -> ChanFrontend -> ChanServer CmdClientUI CmdSer -> IO ())- -> (FactionId -> ChanServer CmdClientAI CmdSerTakeTime+ -> (FactionId -> ChanServer CmdClientAI CmdTakeTimeSer -> IO ()) -> IO ()) -> IO ()@@ -101,7 +101,8 @@ cops@Kind.COps{corule} sdebugCli exeServer = do -- UI config reloaded at each client start. sconfigUI <- mkConfigUI corule- let !sbinding = stdBinding sconfigUI -- evaluate to check for errors+ let stdRuleset = Kind.stdRuleset corule+ !sbinding = stdBinding corule sconfigUI -- evaluate to check for errors sdebugMode = (\dbg -> dbg {sfont = sfont dbg `mplus` Just (configFont sconfigUI)}) .@@ -110,7 +111,7 @@ (\dbg -> dbg {snoAnim = snoAnim dbg `mplus` Just (configNoAnim sconfigUI)}) . (\dbg -> dbg {ssavePrefixCli =- ssavePrefixCli dbg `mplus` Just (configSavePrefix sconfigUI)})+ ssavePrefixCli dbg `mplus` Just (rsavePrefix stdRuleset)}) $ sdebugCli defHist <- defHistory let exeClientUI = executorUI $ loopUI sdebugMode cmdClientUISem
Game/LambdaHack/Client/Action.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TupleSections #-} -- | Game action monads and basic building blocks for human and computer -- player actions. Has no access to the the main action type. -- Does not export the @liftIO@ operation nor a few other implementation@@ -7,12 +8,7 @@ MonadClient( getClient, getsClient, putClient, modifyClient, saveClient ) , MonadClientUI , MonadClientReadServer(..), MonadClientWriteServer(..)- , MonadClientAbort( abortWith, tryWith ) , SessionUI(..), ConnFrontend(..), connFrontend- -- * Various ways to abort action- , abort, abortIfWith, neverMind- -- * Abort exception handlers- , tryRepeatedlyWith, tryIgnore, tryWithSlide -- * Executing actions , mkConfigUI -- * Accessors to the game session Reader and the Perception Reader(-like)@@ -20,63 +16,83 @@ -- * History and report , msgAdd, msgReset, recordHistory -- * Key input- , getKeyOverlayCommand, getInitConfirms+ , getKeyOverlayCommand, getInitConfirms, stopPlayBack, stopRunning -- * Display and key input , displayFrames, displayMore, displayYesNo, displayChoiceUI -- * Generate slideshows- , promptToSlideshow, overlayToSlideshow+ , promptToSlideshow, overlayToSlideshow, overlayToBlankSlideshow -- * Draw frames , drawOverlay, animate -- * Assorted primitives , restoreGame, removeServerSave, displayPush, scoreToSlideshow- , rndToAction, getArenaUI, getLeaderUI- , targetToPos, partAidLeader, partActorLeader+ , rndToAction, getArenaUI, getLeaderUI, targetDescLeader, viewedLevel+ , aidTgtToPos, aidTgtAims, leaderTgtToPos, leaderTgtAims, cursorToPos+ , partAidLeader, partActorLeader, unexploredDepth+ , getCacheBfsAndPath, getCacheBfs, accessCacheBfs, actorAimsPos+ , closestUnknown, closestSmell, furthestKnown, closestTriggers+ , closestItems, closestFoes, actorAbilities , debugPrint ) where +import Control.Arrow ((&&&)) import Control.Concurrent import Control.Concurrent.STM import Control.DeepSeq import Control.Exception.Assert.Sugar import Control.Monad import qualified Control.Monad.State as St-import Control.Monad.Writer.Strict (WriterT, lift, tell) import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import qualified Data.Ini as Ini+import qualified Data.Ini.Reader as Ini+import qualified Data.Ini.Types as Ini+import Data.List import qualified Data.Map.Strict as M import Data.Maybe-import qualified Data.Monoid as Monoid+import Data.Monoid+import Data.Ord import Data.Text (Text) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import System.Directory import System.FilePath import System.Time+import Text.Read import Game.LambdaHack.Client.Action.ActionClass import Game.LambdaHack.Client.Binding import Game.LambdaHack.Client.Config import Game.LambdaHack.Client.Draw-import Game.LambdaHack.Client.HumanCmd import Game.LambdaHack.Client.State+import Game.LambdaHack.Common.Ability (Ability) import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Animation-import qualified Game.LambdaHack.Common.ConfigIO as ConfigIO+import qualified Game.LambdaHack.Common.Effect as Effect import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.HighScore as HighScore+import Game.LambdaHack.Common.HumanCmd import qualified Game.LambdaHack.Common.Key as K import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point+import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Random import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Content.TileKind import qualified Game.LambdaHack.Frontend as Frontend+import Game.LambdaHack.Utils.File debugPrint :: MonadClient m => Text -> m () debugPrint t = do@@ -92,45 +108,6 @@ liftIO $ atomically $ writeTQueue toF (fid, efr) } --- | Reset the state and resume from the last backup point, i.e., invoke--- the failure continuation.-abort :: MonadClientAbort m => m a-abort = abortWith ""---- | Abort and print the given msg if the condition is true.-abortIfWith :: MonadClientAbort m => Bool -> Msg -> m a-abortIfWith True msg = abortWith msg-abortIfWith False _ = abortWith ""---- | Abort and conditionally print the fixed message.-neverMind :: MonadClientAbort m => Bool -> m a-neverMind b = abortIfWith b "never mind"---- | Take a handler and a computation. If the computation fails, the--- handler is invoked and then the computation is retried.-tryRepeatedlyWith :: MonadClientAbort m => (Msg -> m ()) -> m () -> m ()-tryRepeatedlyWith exc m =- tryWith (\msg -> exc msg >> tryRepeatedlyWith exc m) m---- | Try the given computation and silently catch failure.-tryIgnore :: MonadClientAbort m => m () -> m ()-tryIgnore =- tryWith (\msg -> unless (T.null msg)- $ assert `failure` "can't catch failure with message"- `twith` msg)---- | Set the current exception handler. Apart of executing it,--- draw and pass along a slide with the abort message (even if message empty).-tryWithSlide :: (MonadClientAbort m, MonadClientUI m)- => m a -> WriterT Slideshow m a -> WriterT Slideshow m a-tryWithSlide exc h =- let excMsg msg = do- msgReset ""- slides <- promptToSlideshow msg- tell slides- lift exc- in tryWith excMsg h- displayFrame :: MonadClientUI m => Bool -> Maybe SingleFrame -> m () displayFrame isRunning mf = do ConnFrontend{writeConnFrontend} <- getsSession sfconn@@ -142,16 +119,54 @@ promptGetKey :: MonadClientUI m => [K.KM] -> SingleFrame -> m K.KM promptGetKey frontKM frontFr = do- ConnFrontend{..} <- getsSession sfconn- writeConnFrontend Frontend.FrontKey {..}- readConnFrontend+ lastPlayOld <- getsClient slastPlay+ km <- case lastPlayOld of+ km : kms | null frontKM || km `elem` frontKM -> do+ displayFrame False $ Just frontFr+ modifyClient $ \cli -> cli {slastPlay = kms}+ return km+ _ -> do+ unless (null lastPlayOld) stopPlayBack -- something went wrong+ ConnFrontend{..} <- getsSession sfconn+ writeConnFrontend Frontend.FrontKey {..}+ readConnFrontend+ (seqCurrent, seqPrevious, k) <- getsClient slastRecord+ let slastRecord = (km : seqCurrent, seqPrevious, k)+ modifyClient $ \cli -> cli {slastRecord}+ return km +stopPlayBack :: MonadClientUI m => m ()+stopPlayBack = do+ modifyClient $ \cli -> cli+ { slastPlay = []+ , slastRecord = let (seqCurrent, seqPrevious, _) = slastRecord cli+ in (seqCurrent, seqPrevious, 0)+ , swaitTimes = - swaitTimes cli+ }+ stopRunning++stopRunning :: MonadClientUI m => m ()+stopRunning = do+ srunning <- getsClient srunning+ case srunning of+ Nothing -> return ()+ Just RunParams{runLeader} -> do+ -- Switch to the original leader, from before the run start, unless dead.+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ arena <- getArenaUI+ s <- getState+ when (memActor runLeader arena s && not (isSpawnFact fact)) $+ modifyClient $ updateLeader runLeader s+ modifyClient (\cli -> cli { srunning = Nothing })+ -- | Display a slideshow, awaiting confirmation for each slide except the last. getInitConfirms :: MonadClientUI m => ColorMode -> [K.KM] -> Slideshow -> m Bool getInitConfirms dm frontClear slides = do ConnFrontend{..} <- getsSession sfconn- frontSlides <- mapM (drawOverlay dm) $ runSlideshow slides+ let (onBlank, ovs) = slideshow slides+ frontSlides <- mapM (drawOverlay onBlank dm) ovs -- The first two cases are optimizations: case frontSlides of [] -> return True@@ -163,52 +178,6 @@ km <- readConnFrontend return $! km /= K.escKey -getLeaderUI :: MonadClientUI m => m ActorId-getLeaderUI = do- cli <- getClient- case _sleader cli of- Nothing -> assert `failure` "leader expected but not found" `twith` cli- Just leader -> return leader--getArenaUI :: MonadClientUI m => m LevelId-getArenaUI = do- mleader <- getsClient _sleader- case mleader of- Just leader -> getsState $ blid . getActorBody leader- Nothing -> do- side <- getsClient sside- factionD <- getsState sfactionD- let fact = factionD EM.! side- case gquit fact of- Just Status{stDepth} -> return $ toEnum stDepth- Nothing -> do- dungeon <- getsState sdungeon- let (minD, maxD) =- case (EM.minViewWithKey dungeon, EM.maxViewWithKey dungeon) of- (Just ((s, _), _), Just ((e, _), _)) -> (s, e)- _ -> assert `failure` "empty dungeon" `twith` dungeon- return $ max minD $ min maxD $ playerEntry $ gplayer fact---- | Calculate the position of leader's target.-targetToPos :: MonadClientUI m => m (Maybe Point)-targetToPos = do- mleader <- getsClient _sleader- case mleader of- Nothing -> return Nothing- Just leader -> do- scursor <- getsClient scursor- lid <- getsState $ blid . getActorBody leader- target <- getsClient $ getTarget leader- case target of- Just (TPos pos) -> return $ Just pos- Just (TEnemy a _ll) -> do- mem <- getsState $ memActor a lid -- alive and visible?- if mem then do- pos <- getsState $ bpos . getActorBody a- return $ Just pos- else return Nothing- Nothing -> return scursor- -- | Get the key binding. askBinding :: MonadClientUI m => m Binding askBinding = getsSession sbinding@@ -240,14 +209,12 @@ $ EM.lookup lid fper -- | Display an overlay and wait for a human player command.-getKeyOverlayCommand :: MonadClientUI m => Overlay -> m K.KM-getKeyOverlayCommand overlay = do- frame <- drawOverlay ColorFull overlay- keyb <- askBinding+getKeyOverlayCommand :: MonadClientUI m => Bool -> Overlay -> m K.KM+getKeyOverlayCommand onBlank overlay = do+ frame <- drawOverlay onBlank ColorFull overlay -- Give the previous client time to display his frames. liftIO $ threadDelay 1000- km <- promptGetKey [] frame- return $! fromMaybe km $ M.lookup km $ kmacro keyb+ promptGetKey [] frame -- | Push frames or delays to the frame queue. displayFrames :: MonadClientUI m => Frames -> m ()@@ -271,42 +238,42 @@ displayMore dm prompt = do slides <- promptToSlideshow $ prompt <+> moreMsg -- Two frames drawn total (unless 'prompt' very long).- getInitConfirms dm [] $ slides Monoid.<> toSlideshow [[]]+ getInitConfirms dm [] $ slides <> toSlideshow False [[]] -- | Print a yes/no question and return the player's answer. Use black -- and white colours to turn player's attention to the choice. displayYesNo :: MonadClientUI m => ColorMode -> Msg -> m Bool displayYesNo dm prompt = do sli <- promptToSlideshow $ prompt <+> yesnoMsg- frame <- drawOverlay dm $ head $ runSlideshow sli+ frame <- drawOverlay False dm $ head . snd $ slideshow sli getYesNo frame -- TODO: generalize getInitConfirms and displayChoiceUI to a single op -- | Print a prompt and an overlay and wait for a player keypress. -- If many overlays, scroll screenfuls with SPACE. Do not wrap screenfuls -- (in some menus @?@ cycles views, so the user can restart from the top).-displayChoiceUI :: (MonadClientAbort m, MonadClientUI m)- => Msg -> Overlay -> [K.KM] -> m K.KM+displayChoiceUI :: MonadClientUI m+ => Msg -> Overlay -> [K.KM] -> m (Either Slideshow K.KM) displayChoiceUI prompt ov keys = do- slides <- fmap runSlideshow $ overlayToSlideshow (prompt <> ", ESC]") ov+ (_, ovs) <- fmap slideshow $ overlayToSlideshow (prompt <> ", ESC]") ov let legalKeys = [ K.KM {key=K.Space, modifier=K.NoModifier} , K.escKey ] ++ keys- loop [] = neverMind True+ loop [] = fmap Left $ promptToSlideshow "never mind" loop (x : xs) = do- frame <- drawOverlay ColorFull x+ frame <- drawOverlay False ColorFull x km@K.KM {..} <- promptGetKey legalKeys frame case key of- K.Esc -> neverMind True+ K.Esc -> fmap Left $ promptToSlideshow "never mind" K.Space -> loop xs- _ -> return km- loop slides+ _ -> return $ Right km+ loop ovs -- | The prompt is shown after the current message, but not added to history. -- This is useful, e.g., in targeting mode, not to spam history. promptToSlideshow :: MonadClientUI m => Msg -> m Slideshow-promptToSlideshow prompt = overlayToSlideshow prompt []+promptToSlideshow prompt = overlayToSlideshow prompt emptyOverlay -- | The prompt is shown after the current message at the top of each slide. -- Together they may take more than one line. The prompt is not added@@ -315,23 +282,35 @@ overlayToSlideshow :: MonadClientUI m => Msg -> Overlay -> m Slideshow overlayToSlideshow prompt overlay = do lid <- getArenaUI- Level{lysize} <- getLevel lid -- TODO: screen length or viewLevel+ Level{lxsize, lysize} <- getLevel lid -- TODO: screen length or viewLevel sreport <- getsClient sreport- let msg = splitReport (addMsg sreport prompt)- return $! splitOverlay lysize msg overlay+ let msg = splitReport lxsize (addMsg sreport prompt)+ return $! splitOverlay False (lysize + 1) msg overlay +overlayToBlankSlideshow :: MonadClientUI m => Msg -> Overlay -> m Slideshow+overlayToBlankSlideshow prompt overlay = do+ lid <- getArenaUI+ Level{lysize} <- getLevel lid -- TODO: screen length or viewLevel+ return $! splitOverlay True (lysize + 3) (toOverlay [prompt]) overlay+ -- | Draw the current level with the overlay on top.-drawOverlay :: MonadClientUI m => ColorMode -> Overlay -> m SingleFrame-drawOverlay dm over = do+drawOverlay :: MonadClientUI m => Bool -> ColorMode -> Overlay -> m SingleFrame+drawOverlay onBlank dm over = do cops <- getsState scops- stgtMode <- getsClient stgtMode- arena <- getArenaUI- let lid = maybe arena tgtLevelId stgtMode+ lid <- viewedLevel mleader <- getsClient _sleader s <- getState cli <- getClient per <- getPerFid lid- return $! draw dm cops per lid mleader cli s over+ tgtPos <- leaderTgtToPos+ cursorPos <- cursorToPos+ let pathFromLeader leader =+ maybe (return Nothing) (fmap Just . getCacheBfsAndPath leader) tgtPos+ bfsmpath <- maybe (return Nothing) pathFromLeader mleader+ tgtDesc <- maybe (return "------") targetDescLeader mleader+ cursorDesc <- targetDescCursor+ return $! draw onBlank dm cops per lid mleader cursorPos tgtPos+ bfsmpath cli s cursorDesc tgtDesc over -- TODO: if more slides, don't take head, but do as in getInitConfirms, -- but then we have to clear the messages or they get redisplayed@@ -341,8 +320,8 @@ displayPush :: MonadClientUI m => m () displayPush = do sls <- promptToSlideshow ""- let slide = head $ runSlideshow sls- frame <- drawOverlay ColorFull slide+ let slide = head . snd $ slideshow sls+ frame <- drawOverlay False ColorFull slide -- Visually speed up (by remving all empty frames) the show of the sequence -- of the move frames if the player is running. srunning <- getsClient srunning@@ -350,36 +329,43 @@ scoreToSlideshow :: MonadClientUI m => Int -> Status -> m Slideshow scoreToSlideshow total status = do+ fid <- getsClient sside+ fact <- getsState $ (EM.! fid) . sfactionD table <- getsState shigh time <- getsState stime date <- liftIO getClockTime- let showScore (ntable, pos) = HighScore.slideshow ntable pos status- return $! maybe Monoid.mempty showScore- $ HighScore.register table total time status date+ scurDifficulty <- getsClient scurDifficulty+ let showScore (ntable, pos) = HighScore.highSlideshow ntable pos status+ diff | not $ playerUI $ gplayer fact = 0+ | otherwise = scurDifficulty+ return $! maybe mempty showScore+ $ HighScore.register table total time status date diff restoreGame :: MonadClient m => m (Maybe (State, StateClient)) restoreGame = do Kind.COps{corule} <- getsState scops- let pathsDataFile = rpathsDataFile $ Kind.stdRuleset corule+ let stdRuleset = Kind.stdRuleset corule+ pathsDataFile = rpathsDataFile stdRuleset+ cfgUIName = rcfgUIName stdRuleset side <- getsClient sside isAI <- getsClient sisAI prefix <- getsClient $ ssavePrefixCli . sdebugCli- ConfigUI{ configAppDataDir- , configUICfgFile } <- getsClient sconfigUI- let copies = [(configUICfgFile <.> ".default", configUICfgFile <.> ".ini")]+ let copies = [( "GameDefinition" </> cfgUIName <.> "default"+ , cfgUIName <.> "ini" )] name = fromMaybe "save" prefix <.> saveName side isAI- liftIO $ Save.restoreGame name configAppDataDir copies pathsDataFile+ liftIO $ Save.restoreGame name copies pathsDataFile -- | Assuming the client runs on the same machine and for the same -- user as the server, move the server savegame out of the way. removeServerSave :: MonadClient m => m () removeServerSave = do prefix <- getsClient $ ssavePrefixCli . sdebugCli -- hack: assume the same- ConfigUI{configAppDataDir} <- getsClient sconfigUI- let serverSaveFile = configAppDataDir+ dataDir <- liftIO appDataDir+ let serverSaveFile = dataDir </> fromMaybe "save" prefix <.> serverSaveName- liftIO $ renameFile serverSaveFile (serverSaveFile ++ ".bkp")+ bSer <- liftIO $ doesFileExist serverSaveFile+ when bSer $ liftIO $ renameFile serverSaveFile (serverSaveFile <.> "bkp") -- | Invoke pseudo-random computation with the generator kept in the state. rndToAction :: MonadClient m => Rnd a -> m a@@ -400,13 +386,22 @@ cli <- getClient s <- getState per <- getPerFid arena+ tgtPos <- leaderTgtToPos+ cursorPos <- cursorToPos+ let pathFromLeader leader =+ maybe (return Nothing) (fmap Just . getCacheBfsAndPath leader) tgtPos+ bfsmpath <- maybe (return Nothing) pathFromLeader mleader+ tgtDesc <- maybe (return "------") targetDescLeader mleader+ cursorDesc <- targetDescCursor let over = renderReport sreport- topLineOnly = truncateMsg lxsize over- basicFrame = draw ColorFull cops per arena mleader cli s [topLineOnly]+ topLineOnly = truncateToOverlay lxsize over+ basicFrame =+ draw False ColorFull cops per arena mleader+ cursorPos tgtPos bfsmpath cli s cursorDesc tgtDesc topLineOnly snoAnim <- getsClient $ snoAnim . sdebugCli- return $ if fromMaybe False snoAnim- then [Just basicFrame]- else renderAnim lxsize lysize basicFrame anim+ return $! if fromMaybe False snoAnim+ then [Just basicFrame]+ else renderAnim lxsize lysize basicFrame anim -- | The part of speech describing the actor or a special name if a leader -- of the observer's faction. The actor may not be present in the dungeon.@@ -425,43 +420,468 @@ b <- getsState $ getActorBody aid partActorLeader aid b -parseConfigUI :: FilePath -> ConfigIO.CP -> ConfigUI-parseConfigUI dataDir cp =- let mkKey s =- case K.keyTranslate s of- K.Unknown _ ->- assert `failure` "unknown config file key" `twith` (s, cp)- key -> key- mkKM ('C':'T':'R':'L':'-':s) = K.KM {key=mkKey s, modifier=K.Control}- mkKM s = K.KM {key=mkKey s, modifier=K.NoModifier}- configCommands =- let mkCommand (key, def) = (mkKM key, read def :: HumanCmd)- section = ConfigIO.getItems cp "commands"+parseConfigUI :: Ini.Config -> ConfigUI+parseConfigUI cfg =+ let configCommands =+ let mkCommand (ident, keydef) =+ case stripPrefix "Macro_" ident of+ Just _ ->+ let (key, def) = read keydef+ in (K.mkKM key, def :: (CmdCategory, HumanCmd))+ Nothing -> assert `failure` "wrong macro id" `twith` ident+ section = Ini.allItems "extra_commands" cfg in map mkCommand section- configAppDataDir = dataDir- configUICfgFile = "config.ui"- configSavePrefix = ConfigIO.get cp "file" "savePrefix"- configMacros =- let trMacro (from, to) =- let fromTr = mkKM from- toTr = mkKM to- in if fromTr == toTr- then assert `failure` "degenerate alias" `twith` toTr- else (fromTr, toTr)- section = ConfigIO.getItems cp "macros"- in map trMacro section- configFont = ConfigIO.get cp "ui" "font"- configHistoryMax = ConfigIO.get cp "ui" "historyMax"- configMaxFps = ConfigIO.get cp "ui" "maxFps"- configNoAnim = ConfigIO.get cp "ui" "noAnim"+ configHeroNames =+ let toNumber (ident, name) =+ case stripPrefix "HeroName_" ident of+ Just n -> (read n, T.pack name)+ Nothing -> assert `failure` "wrong hero name id" `twith` ident+ section = Ini.allItems "hero_names" cfg+ in map toNumber section+ getOption :: forall a. Read a => String -> a+ getOption optionName =+ let lookupFail :: forall b. String -> b+ lookupFail err =+ assert `failure` ("config file access failed:" <+> T.pack err)+ `twith` (optionName, cfg)+ s = fromMaybe (lookupFail "") $ Ini.getOption "ui" optionName cfg+ in either lookupFail id $ readEither s+ configFont = getOption "font"+ configHistoryMax = getOption "historyMax"+ configMaxFps = getOption "maxFps"+ configNoAnim = getOption "noAnim"+ configRunStopMsgs = getOption "runStopMsgs" in ConfigUI{..} -- | Read and parse UI config file. mkConfigUI :: Kind.Ops RuleKind -> IO ConfigUI mkConfigUI corule = do- let cpUIDefault = rcfgUIDefault $ Kind.stdRuleset corule- dataDir <- ConfigIO.appDataDir- cpUI <- ConfigIO.mkConfig cpUIDefault $ dataDir </> "config.ui.ini"- let conf = parseConfigUI dataDir cpUI- -- Catch syntax errors ASAP,+ let stdRuleset = Kind.stdRuleset corule+ cfgUIName = rcfgUIName stdRuleset+ commentsUIDefault = init $ map (drop 2) $ lines $ rcfgUIDefault stdRuleset -- TODO: init is a hack until Ini accepts empty files+ sUIDefault = unlines commentsUIDefault+ cfgUIDefault = either (assert `failure`) id $ Ini.parse sUIDefault+ dataDir <- appDataDir+ let userPath = dataDir </> cfgUIName <.> "ini"+ cfgUser <- do+ cpExists <- doesFileExist userPath+ if not cpExists+ then return Ini.emptyConfig+ else do+ sUser <- readFile userPath+ return $! either (assert `failure`) id $ Ini.parse sUser+ let cfgUI = M.unionWith M.union cfgUser cfgUIDefault -- user cfg preferred+ conf = parseConfigUI cfgUI+ -- Catch syntax errors in complex expressions ASAP, return $! deepseq conf conf++-- | Get cached BFS data and path or, if not stored, generate,+-- store and return. Due to laziness, they are not calculated until needed.+getCacheBfsAndPath :: forall m. MonadClient m+ => ActorId -> Point+ -> m (PointArray.Array BfsDistance, Maybe [Point])+getCacheBfsAndPath aid target = do+ seps <- getsClient seps+ let pathAndStore :: PointArray.Array BfsDistance+ -> m (PointArray.Array BfsDistance, Maybe [Point])+ pathAndStore bfs = do+ computePath <- computePathBFS aid+ let mpath = computePath target seps bfs+ modifyClient $ \cli ->+ cli {sbfsD = EM.insert aid (bfs, target, seps, mpath) (sbfsD cli)}+ return (bfs, mpath)+ mbfs <- getsClient $ EM.lookup aid . sbfsD+ case mbfs of+ Just (bfs, targetOld, sepsOld, mpath) | targetOld == target+ && sepsOld == seps ->+ return (bfs, mpath)+ Just (bfs, _, _, _) -> pathAndStore bfs+ Nothing -> do+ bfs <- computeBFS aid+ pathAndStore bfs++getCacheBfs :: MonadClient m => ActorId -> m (PointArray.Array BfsDistance)+{-# INLINE getCacheBfs #-}+getCacheBfs aid = do+ mbfs <- getsClient $ EM.lookup aid . sbfsD+ case mbfs of+ Just (bfs, _, _, _) -> return bfs+ Nothing -> fmap fst $ getCacheBfsAndPath aid (Point 0 0)++computeBFS :: MonadClient m => ActorId -> m (PointArray.Array BfsDistance)+computeBFS = computeAnythingBFS $ \isEnterable passUnknown aid -> do+ b <- getsState $ getActorBody aid+ Level{lxsize, lysize} <- getLevel $ blid b+ let origin = bpos b+ vInitial = PointArray.replicateA lxsize lysize apartBfs+ -- Here we don't want '$!', because we want the BFS data lazy.+ return ${-keep it!-} fillBfs isEnterable passUnknown origin vInitial++computePathBFS :: MonadClient m+ => ActorId+ -> m (Point -> Int -> PointArray.Array BfsDistance+ -> Maybe [Point])+computePathBFS = computeAnythingBFS $ \isEnterable passUnknown aid -> do+ b <- getsState $ getActorBody aid+ let origin = bpos b+ -- Here we don't want '$!', because we want the BFS data lazy.+ return ${-keep it!-} findPathBfs isEnterable passUnknown origin++computeAnythingBFS :: MonadClient m+ => ((Point -> Point -> MoveLegal)+ -> (Point -> Point -> Bool)+ -> ActorId+ -> m a)+ -> ActorId+ -> m a+computeAnythingBFS fAnything aid = do+ cops@Kind.COps{cotile=cotile@Kind.Ops{ouniqGroup}} <- getsState scops+ b <- getsState $ getActorBody aid+ lvl <- getLevel $ blid b+ smarkSuspect <- getsClient smarkSuspect+ sisAI <- getsClient sisAI+ -- We treat doors as an open tile and don't add an extra step for opening+ -- the doors, because other actors open and use them, too,+ -- so it's amortized. We treat unknown tiles specially.+ let -- Suspect tiles treated as a kind of unknown.+ passSuspect = smarkSuspect || sisAI -- AI checks suspects ASAP+ unknownId = ouniqGroup "unknown space"+ chAccess = checkAccess cops lvl+ chDoorAccess = checkDoorAccess cops lvl+ conditions = catMaybes [chAccess, chDoorAccess]+ -- Legality of move from a known tile, assuming doors freely openable.+ isEnterable :: Point -> Point -> MoveLegal+ isEnterable spos tpos =+ let tt = lvl `at` tpos+ allOK = all (\f -> f spos tpos) conditions+ in if tt == unknownId+ then if allOK+ then MoveToUnknown+ else MoveBlocked+ else if Tile.isSuspect cotile tt+ then if passSuspect && allOK+ then MoveToUnknown+ else MoveBlocked+ else if Tile.isPassable cotile tt && allOK+ then MoveToOpen+ else MoveBlocked+ -- Legality of move from an unknown tile, assuming unknown are open.+ passUnknown :: Point -> Point -> Bool+ passUnknown = case chAccess of -- spos is unknown, so not a door+ Nothing -> \_ tpos -> let tt = lvl `at` tpos+ in tt == unknownId+ || passSuspect && Tile.isSuspect cotile tt+ Just ch -> \spos tpos -> let tt = lvl `at` tpos+ in (tt == unknownId+ || passSuspect+ && Tile.isSuspect cotile tt)+ && ch spos tpos+ fAnything isEnterable passUnknown aid++accessCacheBfs :: MonadClient m => ActorId -> Point -> m (Maybe Int)+{-# INLINE accessCacheBfs #-}+accessCacheBfs aid target = do+ bfs <- getCacheBfs aid+ return $! accessBfs bfs target++actorAimsPos :: MonadClient m => ActorId -> Point -> m Bool+{-# INLINE actorAimsPos #-}+actorAimsPos aid target = do+ bfs <- getCacheBfs aid+ b <- getsState $ getActorBody aid+ return $! posAimsPos bfs (bpos b) target++targetDesc :: MonadClientUI m => Maybe Target -> m Text+targetDesc target = do+ lidV <- viewedLevel+ mleader <- getsClient _sleader+ case target of+ Just (TEnemy a _) ->+ getsState $ bname . getActorBody a+ Just (TEnemyPos _ lid p _) ->+ return $! if lid == lidV+ then "hot spot" <+> (T.pack . show) p+ else "a hot spot on level" <+> tshow (abs $ fromEnum lid)+ Just (TPoint lid p) ->+ return $! if lid == lidV+ then "exact spot" <+> (T.pack . show) p+ else "an exact spot on level" <+> tshow (abs $ fromEnum lid)+ Just TVector{} ->+ case mleader of+ Nothing -> return "a relative shift"+ Just aid -> do+ tgtPos <- aidTgtToPos aid lidV target+ let invalidMsg = "an invalid relative shift"+ validMsg p = "shift to" <+> (T.pack . show) p+ return $! maybe invalidMsg validMsg tgtPos+ Nothing -> return "cursor location"++targetDescLeader :: MonadClientUI m => ActorId -> m Text+targetDescLeader leader = do+ tgt <- getsClient $ getTarget leader+ targetDesc tgt++targetDescCursor :: MonadClientUI m => m Text+targetDescCursor = do+ scursor <- getsClient scursor+ targetDesc $ Just scursor++getLeaderUI :: MonadClientUI m => m ActorId+getLeaderUI = do+ cli <- getClient+ case _sleader cli of+ Nothing -> assert `failure` "leader expected but not found" `twith` cli+ Just leader -> return leader++getArenaUI :: MonadClientUI m => m LevelId+getArenaUI = do+ mleader <- getsClient _sleader+ case mleader of+ Just leader -> getsState $ blid . getActorBody leader+ Nothing -> do+ side <- getsClient sside+ factionD <- getsState sfactionD+ let fact = factionD EM.! side+ case gquit fact of+ Just Status{stDepth} -> return $! toEnum stDepth+ Nothing -> do+ dungeon <- getsState sdungeon+ let (minD, maxD) =+ case (EM.minViewWithKey dungeon, EM.maxViewWithKey dungeon) of+ (Just ((s, _), _), Just ((e, _), _)) -> (s, e)+ _ -> assert `failure` "empty dungeon" `twith` dungeon+ return $! max minD $ min maxD $ playerEntry $ gplayer fact++viewedLevel :: MonadClientUI m => m LevelId+viewedLevel = do+ arena <- getArenaUI+ stgtMode <- getsClient stgtMode+ return $! maybe arena tgtLevelId stgtMode++-- | Calculate the position of an actor's target.+aidTgtToPos :: MonadClient m+ => ActorId -> LevelId -> Maybe Target -> m (Maybe Point)+aidTgtToPos aid lidV tgt =+ case tgt of+ Just (TEnemy a _) -> do+ body <- getsState $ getActorBody a+ return $! if blid body == lidV+ then Just (bpos body)+ else Nothing+ Just (TEnemyPos _ lid p _) ->+ return $! if lid == lidV then Just p else Nothing+ Just (TPoint lid p) ->+ return $! if lid == lidV then Just p else Nothing+ Just (TVector v) -> do+ b <- getsState $ getActorBody aid+ Level{lxsize, lysize} <- getLevel lidV+ let shifted = shiftBounded lxsize lysize (bpos b) v+ return $! if shifted == bpos b && v /= Vector 0 0+ then Nothing+ else Just shifted+ Nothing -> do+ scursor <- getsClient scursor+ aidTgtToPos aid lidV $ Just scursor++-- | Check whether one is permitted to aim at a target+-- (this is only checked for actors; positions let player+-- shoot at obstacles, e.g., to destroy them).+-- This assumes @aidTgtToPos@ does not return @Nothing@.+--+-- Note: Perception is not enough for the check,+-- because the target actor can be obscured by a glass wall+-- or be out of sight range, but in weapon range.+aidTgtAims :: MonadClient m+ => ActorId -> LevelId -> Maybe Target -> m (Maybe Text)+aidTgtAims aid lidV tgt = do+ case tgt of+ Just (TEnemy a _) -> do+ body <- getsState $ getActorBody a+ let pos = bpos body+ b <- getsState $ getActorBody aid+ if blid b == lidV then do+ aims <- actorAimsPos aid pos+ if aims+ then return Nothing+ else return $ Just "aiming line to the opponent blocked"+ else return $ Just "target opponent not on this level"+ Just TEnemyPos{} -> return $ Just "target opponent not visible"+ Just TPoint{} -> return Nothing+ Just TVector{} -> return Nothing+ Nothing -> do+ scursor <- getsClient scursor+ aidTgtAims aid lidV $ Just scursor++leaderTgtToPos :: MonadClientUI m => m (Maybe Point)+leaderTgtToPos = do+ lidV <- viewedLevel+ mleader <- getsClient _sleader+ case mleader of+ Nothing -> return Nothing+ Just aid -> do+ tgt <- getsClient $ getTarget aid+ aidTgtToPos aid lidV tgt++leaderTgtAims :: MonadClientUI m => m (Maybe Text)+leaderTgtAims = do+ lidV <- viewedLevel+ mleader <- getsClient _sleader+ case mleader of+ Nothing -> return $ Just "no leader to target with"+ Just aid -> do+ tgt <- getsClient $ getTarget aid+ aidTgtAims aid lidV tgt++cursorToPos :: MonadClientUI m => m (Maybe Point)+cursorToPos = do+ lidV <- viewedLevel+ mleader <- getsClient _sleader+ scursor <- getsClient scursor+ case mleader of+ Nothing -> return Nothing+ Just aid -> aidTgtToPos aid lidV $ Just scursor++-- | Furthest (wrt paths) known position, except under the actor.+furthestKnown :: MonadClient m => ActorId -> m (Maybe Point)+furthestKnown aid = do+ bfs <- getCacheBfs aid+ getMaxIndex <- rndToAction $ oneOf [ PointArray.maxIndexA+ , PointArray.maxLastIndexA ]+ let furthestPos = getMaxIndex bfs+ dist = bfs PointArray.! furthestPos+ return $! if dist <= apartBfs+ then assert `failure` (aid, furthestPos, dist)+ else if dist == succ apartBfs -- bpos of aid+ then Nothing+ else Just furthestPos++-- | Closest reachable unknown tile position, if any.+closestUnknown :: MonadClient m => ActorId -> m (Maybe Point)+closestUnknown aid = do+ bfs <- getCacheBfs aid+ getMinIndex <- rndToAction $ oneOf [ PointArray.minIndexA+ , PointArray.minLastIndexA ]+ let closestPos = getMinIndex bfs+ dist = bfs PointArray.! closestPos+ if dist >= apartBfs then do+ body <- getsState $ getActorBody aid+ smarkSuspect <- getsClient smarkSuspect+ sisAI <- getsClient sisAI+ let passSuspect = smarkSuspect || sisAI+ when passSuspect $ -- explored fully, including suspect tiles+ modifyClient $ \cli ->+ cli {sexplored = ES.insert (blid body) (sexplored cli)}+ return Nothing+ else return $ Just closestPos++-- TODO: this is costly, because target has to be changed every+-- turn when walking along trail. But inverting the sort and going+-- to the newest smell, while sometimes faster, may result in many+-- actors following the same trail, unless we wipe the trail as soon+-- as target is assigned (but then we don't know if we should keep the target+-- or not, because somebody already followed it). OTOH, trails are not+-- common and so if wiped they can't incur a large total cost.+-- TODO: remove targets where the smell is likely to get too old by the time+-- the actor gets there.+-- | Finds smells closest to the actor, except under the actor.+closestSmell :: MonadClient m => ActorId -> m [(Int, (Point, Tile.SmellTime))]+closestSmell aid = do+ body <- getsState $ getActorBody aid+ Level{lsmell} <- getLevel $ blid body+ let smells = EM.assocs lsmell+ case smells of+ [] -> return []+ _ -> do+ bfs <- getCacheBfs aid+ let ts = mapMaybe (\x@(p, _) -> fmap (,x) (accessBfs bfs p)) smells+ ds = filter (\(d, _) -> d /= 0) ts -- bpos of aid+ return $! sortBy (comparing (fst &&& timeNegate . snd . snd)) ds++-- TODO: We assume linear dungeon in @unexploredD@,+-- because otherwise we'd need to calculate shortest paths in a graph, etc.+-- | Closest (wrt paths) triggerable open tiles.+-- The second argument can ever be true only if there's+-- no escape from the dungeon.+closestTriggers :: MonadClient m => Maybe Bool -> Bool -> ActorId -> m [Point]+closestTriggers onlyDir exploredToo aid = do+ Kind.COps{cotile} <- getsState scops+ body <- getsState $ getActorBody aid+ lvl <- getLevel $ blid body+ dungeon <- getsState sdungeon+ explored <- getsClient sexplored+ unexploredD <- unexploredDepth+ let allExplored = ES.size explored == EM.size dungeon+ unexUp = onlyDir /= Just False && unexploredD 1 (blid body)+ unexDown = onlyDir /= Just True && unexploredD (-1) (blid body)+ unexEffect (Effect.Ascend p) = if p > 0 then unexUp else unexDown+ unexEffect _ =+ -- Escape (or guard) only after exploring, for high score, etc.+ allExplored+ isTrigger+ | exploredToo = \t -> Tile.isWalkable cotile t+ && not (null $ Tile.causeEffects cotile t)+ | otherwise = \t -> Tile.isWalkable cotile t+ && any unexEffect (Tile.causeEffects cotile t)+ f :: [Point] -> Point -> Kind.Id TileKind -> [Point]+ f acc p t = if isTrigger t then p : acc else acc+ let triggersAll = PointArray.ifoldlA f [] $ ltile lvl+ -- Don't target stairs under the actor. Most of the time they+ -- are blocked and stay so, so we seek other stairs, if any.+ -- If no other stairs in this direction, let's wait here.+ triggers | length triggersAll > 1 = delete (bpos body) triggersAll+ | otherwise = triggersAll+ case triggers of+ [] -> return []+ _ -> do+ bfs <- getCacheBfs aid+ let ds = mapMaybe (\p -> fmap (,p) (accessBfs bfs p)) triggers+ return $! map snd $ sortBy (comparing fst) ds++unexploredDepth :: MonadClient m => m (Int -> LevelId -> Bool)+unexploredDepth = do+ dungeon <- getsState sdungeon+ explored <- getsClient sexplored+ let allExplored = ES.size explored == EM.size dungeon+ unexploredD p =+ let unex lid = allExplored && lescape (dungeon EM.! lid)+ || ES.notMember lid explored+ || unexploredD p lid+ in any unex . ascendInBranch dungeon p+ return unexploredD++-- | Closest (wrt paths) items.+closestItems :: MonadClient m => ActorId -> m ([(Int, (Point, ItemBag))])+closestItems aid = do+ body <- getsState $ getActorBody aid+ Level{lfloor} <- getLevel $ blid body+ let items = EM.assocs lfloor+ case items of+ [] -> return []+ _ -> do+ bfs <- getCacheBfs aid+ let ds = mapMaybe (\x@(p, _) -> fmap (,x) (accessBfs bfs p)) items+ return $! sortBy (comparing fst) ds++-- | Closest (wrt paths) enemy actors.+closestFoes :: MonadClient m => ActorId -> m [(Int, (ActorId, Actor))]+closestFoes aid = do+ body <- getsState $ getActorBody aid+ fact <- getsState $ \s -> sfactionD s EM.! bfid body+ foes <- getsState $ actorNotProjAssocs (isAtWar fact) (blid body)+ case foes of+ [] -> return []+ _ -> do+ bfs <- getCacheBfs aid+ let ds = mapMaybe (\x@(_, b) -> fmap (,x) (accessBfs bfs (bpos b))) foes+ return $! sortBy (comparing fst) ds++actorAbilities :: MonadClient m => ActorId -> Maybe ActorId -> m [Ability]+actorAbilities aid mleader = do+ Kind.COps{ coactor=Kind.Ops{okind}+ , cofaction=Kind.Ops{okind=fokind} } <- getsState scops+ body <- getsState $ getActorBody aid+ fact <- getsState $ (EM.! bfid body) . sfactionD+ let factionAbilities+ | Just aid == mleader = fAbilityLeader $ fokind $ gkind fact+ | otherwise = fAbilityOther $ fokind $ gkind fact+ return $! acanDo (okind $ bkind body) `intersect` factionAbilities
Game/LambdaHack/Client/Action/ActionClass.hs view
@@ -4,15 +4,12 @@ -- and 'TypeAction'. module Game.LambdaHack.Client.Action.ActionClass where -import Control.Monad.Writer.Strict (WriterT (WriterT), lift, runWriterT)-import Data.Monoid import qualified Game.LambdaHack.Common.Key as K import Game.LambdaHack.Client.Binding import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Faction-import Game.LambdaHack.Common.Msg import Game.LambdaHack.Frontend (FrontReq) -- | The information that is constant across a client playing session,@@ -42,37 +39,14 @@ liftIO :: IO a -> m a saveClient :: m () -instance (Monoid a, MonadClient m) => MonadClient (WriterT a m) where- getClient = lift getClient- getsClient = lift . getsClient- modifyClient = lift . modifyClient- putClient = lift . putClient- liftIO = lift . liftIO- saveClient = lift saveClient- class MonadClient m => MonadClientUI m where getsSession :: (SessionUI -> a) -> m a -instance (Monoid a, MonadClientUI m) => MonadClientUI (WriterT a m) where- getsSession = lift . getsSession- class MonadClient m => MonadClientReadServer c m | m -> c where readServer :: m c class MonadClient m => MonadClientWriteServer d m | m -> d where writeServer :: d -> m ()---- | The bottom of the action monads class semilattice.-class MonadClient m => MonadClientAbort m where- -- Set the current exception handler. First argument is the handler,- -- second is the computation the handler scopes over.- tryWith :: (Msg -> m a) -> m a -> m a- -- Abort with the given message.- abortWith :: Msg -> m a--instance (Monoid a, MonadClientAbort m) => MonadClientAbort (WriterT a m) where- tryWith exc m = WriterT $ tryWith (runWriterT . exc) (runWriterT m)- abortWith = lift . abortWith saveName :: FactionId -> Bool -> String saveName side isAI =
Game/LambdaHack/Client/Action/ActionType.hs view
@@ -1,133 +1,92 @@-{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances, GeneralizedNewtypeDeriving,+ MultiParamTypeClasses #-} -- | The main game action monad type implementation. Just as any other -- component of the library, this implementation can be substituted. -- This module should not be imported anywhere except in 'Action' -- to expose the executor to any code using the library. module Game.LambdaHack.Client.Action.ActionType- ( FunActionCli, ActionCli, executorCli+ ( ActionCli, executorCli ) where import Control.Applicative import Control.Concurrent.STM-import Control.Monad-import qualified Data.EnumMap.Strict as EM+import qualified Control.Monad.IO.Class as IO+import Control.Monad.Trans.State.Strict hiding (State) import Data.Maybe-import qualified Data.Text as T import System.FilePath-import qualified System.Random as R import Game.LambdaHack.Client.Action.ActionClass-import Game.LambdaHack.Client.Config import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Animation import Game.LambdaHack.Common.ClientCmd-import Game.LambdaHack.Common.Msg import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State --- | The type of the function inside any client action.-type FunActionCli c d a =- SessionUI -- ^ client UI setup data- -> (ChanServer c d, Save.ChanSave (State, StateClient))- -- ^ this client connection information- -> (State -> StateClient -> a -> IO ())- -- ^ continuation- -> (R.StdGen -> Msg -> IO ()) -- ^ failure/reset continuation- -> State -- ^ current local state- -> StateClient -- ^ current client state- -> IO ()---- | Client parts of actions of human and computer player characters.-newtype ActionCli c d a = ActionCli {runActionCli :: FunActionCli c d a}---- | Invokes the action continuation on the provided argument.-returnActionCli :: a -> ActionCli c d a-returnActionCli x = ActionCli (\_c _d k _a s cli -> k s cli x)---- | Distributes the session and shutdown continuation,--- threads the state and history.-bindActionCli :: ActionCli c d a -> (a -> ActionCli c d b) -> ActionCli c d b-bindActionCli m f = ActionCli (\c d k a s cli ->- let next ns ncli x =- runActionCli (f x) c d k a ns ncli- in runActionCli m c d next a s cli)--instance Monad (ActionCli c d) where- return = returnActionCli- (>>=) = bindActionCli--instance Applicative (ActionCli c d) where- pure = return- (<*>) = ap---- TODO: make sure fmap is inlined and all else is inlined here and elsewhere-instance Functor (ActionCli c d) where- fmap f m =- ActionCli (\c d k a s cli ->- runActionCli m c d (\s' cli' ->- k s' cli' . f) a s cli)+data CliState c d = CliState+ { cliState :: !State -- ^ current global state+ , cliClient :: !StateClient -- ^ current client state+ , cliDict :: !(ChanServer c d) -- ^ this client connection information+ , cliToSave :: !(Save.ChanSave (State, StateClient))+ -- ^ connection to the save thread+ , cliSession :: SessionUI -- ^ UI setup data, empty for AI clients+ } -instance MonadClientAbort (ActionCli c d) where- tryWith exc m =- ActionCli (\c d k a s cli ->- let runA srandom msg =- runActionCli (exc msg) c d k a s cli {srandom}- in runActionCli m c d k runA s cli)- abortWith msg = ActionCli (\_c _d _k a _s cli -> a (srandom cli) msg)+-- | Server state transformation monad.+newtype ActionCli c d a =+ ActionCli {runActionCli :: StateT (CliState c d) IO a}+ deriving (Monad, Functor, Applicative) instance MonadActionRO (ActionCli c d) where- getState = ActionCli (\_c _d k _a s cli -> k s cli s)- getsState = (`fmap` getState)+ getState = ActionCli $ gets cliState+ getsState f = ActionCli $ gets $ f . cliState instance MonadAction (ActionCli c d) where- modifyState f = ActionCli (\_c _d k _a s cli -> k (f s) cli ())- putState = modifyState . const+ modifyState f = ActionCli $ state $ \cliS ->+ let newCliS = cliS {cliState = f $ cliState cliS}+ in newCliS `seq` ((), newCliS)+ putState s = ActionCli $ state $ \cliS ->+ let newCliS = cliS {cliState = s}+ in newCliS `seq` ((), newCliS) instance MonadClient (ActionCli c d) where- getClient = ActionCli (\_c _d k _a s cli -> k s cli cli)- getsClient = (`fmap` getClient)- modifyClient f = ActionCli (\_c _d k _a s cli -> k s (f cli) ())- putClient = modifyClient . const- liftIO x = ActionCli (\_c _d k _a s cli -> x >>= k s cli)- saveClient = ActionCli (\_c (_, toSave) k _a s cli -> do- Save.saveToChan toSave (s, cli)- k s cli ())+ getClient = ActionCli $ gets cliClient+ getsClient f = ActionCli $ gets $ f . cliClient+ modifyClient f = ActionCli $ state $ \cliS ->+ let newCliS = cliS {cliClient = f $ cliClient cliS}+ in newCliS `seq` ((), newCliS)+ putClient s = ActionCli $ state $ \cliS ->+ let newCliS = cliS {cliClient = s}+ in newCliS `seq` ((), newCliS)+ liftIO = ActionCli . IO.liftIO+ saveClient = ActionCli $ do+ s <- gets cliState+ cli <- gets cliClient+ toSave <- gets cliToSave+ IO.liftIO $ Save.saveToChan toSave (s, cli) instance MonadClientUI (ActionCli c d) where- getsSession f = ActionCli (\c _d k _a s cli -> k s cli (f c))+ getsSession f = ActionCli $ gets $ f . cliSession instance MonadClientReadServer c (ActionCli c d) where- readServer =- ActionCli (\_c (ChanServer{..}, _) k _a s cli -> do- ccmd <- atomically . readTQueue $ fromServer- k s cli ccmd)+ readServer = ActionCli $ do+ ChanServer{fromServer} <- gets cliDict+ IO.liftIO $ atomically . readTQueue $ fromServer instance MonadClientWriteServer d (ActionCli c d) where- writeServer scmd =- ActionCli (\_c (ChanServer{..}, _) k _a s cli -> do- atomically . writeTQueue toServer $ scmd- k s cli ())+ writeServer scmd = ActionCli $ do+ ChanServer{toServer} <- gets cliDict+ IO.liftIO $ atomically . writeTQueue toServer $ scmd -- | Init the client, then run an action, with a given session, -- state and history, in the @IO@ monad. executorCli :: ActionCli c d () -> SessionUI -> State -> StateClient -> ChanServer c d -> IO ()-executorCli m sess s cli d =+executorCli m cliSession cliState cliClient cliDict = let saveFile (_, cli2) =- configAppDataDir (sconfigUI cli2)- </> fromMaybe "save" (ssavePrefixCli (sdebugCli cli2))+ fromMaybe "save" (ssavePrefixCli (sdebugCli cli2)) <.> saveName (sside cli2) (sisAI cli2)- exe toSave =- runActionCli m- sess- (d, toSave)- (\_ _ _ -> return ())- (\_ msg -> let err = "unhandled abort for client"- <+> showT (sfactionD s EM.! sside cli)- <+> ":" <+> msg- in fail $ T.unpack err)- s- cli+ exe cliToSave =+ evalStateT (runActionCli m) CliState{..} in Save.wrapInSaves saveFile exe
Game/LambdaHack/Client/AtomicSemCli.hs view
@@ -6,11 +6,12 @@ , drawCmdAtomicUI, drawSfxAtomicUI ) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Maybe-import qualified Data.Monoid as Monoid+import Data.Monoid import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.Action@@ -33,11 +34,13 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.State+import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Content.TileKind-import Control.Exception.Assert.Sugar -- * CmdAtomicAI @@ -46,9 +49,18 @@ -- of commands kept for each command received. cmdAtomicFilterCli :: MonadClient m => CmdAtomic -> m [CmdAtomic] cmdAtomicFilterCli cmd = case cmd of+ MoveActorA aid _ toP -> do+ cmdSml <- deleteSmell aid toP+ return $ [cmd] ++ cmdSml+ DisplaceActorA source target -> do+ bs <- getsState $ getActorBody source+ bt <- getsState $ getActorBody target+ cmdSource <- deleteSmell source (bpos bt)+ cmdTarget <- deleteSmell target (bpos bs)+ return $ [cmd] ++ cmdSource ++ cmdTarget AlterTileA lid p fromTile toTile -> do- Kind.COps{cotile = Kind.Ops{okind}} <- getsState scops- lvl@Level{lxsize} <- getLevel lid+ Kind.COps{cotile=Kind.Ops{okind}} <- getsState scops+ lvl <- getLevel lid let t = lvl `at` p if t == fromTile then return [cmd]@@ -61,7 +73,7 @@ let subject = "" -- a hack, we we don't handle adverbs well verb = "turn into" msg = makeSentence [ "the", MU.Text $ tname $ okind t- , "at position", MU.Text $ showPoint lxsize p+ , "at position", MU.Text $ tshow p , "suddenly" -- adverb , MU.SubjectVerbSg subject verb , MU.AW $ MU.Text $ tname $ okind toTile ]@@ -72,17 +84,40 @@ b <- getsState $ getActorBody aid lvl <- getLevel $ blid b let t = lvl `at` p- if t == toTile- then -- Already knows the tile fully.- return []- else if t == fromTile- then -- Fully ignorant. (No intermediate knowledge possible.)- return [ AlterTileA (blid b) p fromTile toTile -- reveal tile- , cmd -- show the message- ]+ return $!+ if t == fromTile+ then -- Fully ignorant. (No intermediate knowledge possible.)+ [ cmd -- show the message+ , AlterTileA (blid b) p fromTile toTile -- reveal tile+ ]+ else if t == toTile+ then [cmd] -- Already knows the tile fully, only confirm. else -- Misguided. assert `failure` "LoseTile fails to reset memory" `twith` (aid, p, fromTile, toTile, b, t, cmd)+ SpotTileA lid ts -> do+ Kind.COps{cotile} <- getsState scops+ lvl <- getLevel lid+ -- We ignore the server resending us hidden versions of the tiles+ -- (and resending us the same data we already got).+ -- If the tiles are changed to other variants of the hidden tile,+ -- we can still verify by searching, and the UI warns us "obscured".+ let notKnown (p, t) = let tClient = lvl `at` p+ in t /= tClient+ && (not (isSecretPos lvl p)+ || t /= Tile.hideAs cotile tClient)+ newTs = filter notKnown ts+ return $! if null newTs then [] else [SpotTileA lid newTs]+ AlterSmellA lid p fromSm _toSm -> do+ lvl <- getLevel lid+ let msml = EM.lookup p $ lsmell lvl+ return $ if msml /= fromSm then+ -- Revert to the server smell before server command executes.+ -- This is needed due to our hacky removal of traversed smells+ -- in @deleteSmell@.+ [AlterSmellA lid p msml fromSm, cmd]+ else+ [cmd] DiscoverA _ _ iid _ -> do disco <- getsClient sdisco item <- getsState $ getItemBody iid@@ -95,14 +130,14 @@ if jkindIx item `EM.notMember` disco then return [] else return [cmd]- PerceptionA lid outPA inPA -> do+ PerceptionA lid outPer inPer -> do -- Here we cheat by setting a new perception outright instead of -- in @cmdAtomicSemCli@, to avoid computing perception twice. -- TODO: try to assert similar things as for @atomicRemember@: -- that posCmdAtomic of all the Lose* commands was visible in old Per, -- but is not visible any more. perOld <- getPerFid lid- perceptionA lid outPA inPA+ perceptionA lid outPer inPer perNew <- getPerFid lid s <- getState fid <- getsClient sside@@ -111,12 +146,13 @@ -- and keep only those where seenAtomicCli is True; this is even -- cheaper than repeated posToActor (until it's optimized). let outFov = totalVisible perOld ES.\\ totalVisible perNew- outPrio = mapMaybe (\p -> posToActor p lid s) $ ES.elems outFov- fActor aid =- let b = getActorBody aid s- in if bfid b == fid -- optimization; precludes DominateActorA- then Nothing- else Just $ LoseActorA aid b (getActorItem aid s)+ outPrio = concatMap (\p -> posToActors p lid s) $ ES.elems outFov+ fActor ((aid, b), ais) =+ -- TODO: instead of bproj, check that actor sees himself.+ if not (bproj b) && bfid b == fid+ then Nothing -- optimization: the actor is soon lost anyway,+ -- e.g., via DominateActorA, so don't bother+ else Just $ LoseActorA aid b ais outActor = mapMaybe fActor outPrio -- Wipe out remembered items on tiles that now came into view. Level{lfloor, lsmell} <- getLevel lid@@ -146,18 +182,29 @@ assert (allB (not . seenOld) psItemSmell) skip -- Verify that we forget only currently seen items and smell. assert (allB seenNew psItemSmell) skip- return $ cmd : outActor ++ inItem ++ inSmell+ return $! cmd : outActor ++ inItem ++ inSmell _ -> return [cmd] +deleteSmell :: MonadClient m => ActorId -> Point -> m [CmdAtomic]+deleteSmell aid pos = do+ Kind.COps{coactor = Kind.Ops{okind}} <- getsState scops+ b <- getsState $ getActorBody aid+ let canSmell = asmell $ okind $ bkind b+ if canSmell then do+ lvl <- getLevel $ blid b+ let msml = EM.lookup pos $ lsmell lvl+ return $+ maybe [] (\sml -> [AlterSmellA (blid b) pos (Just sml) Nothing]) msml+ else return []+ -- | Effect of atomic actions on client state is calculated -- in the global state before the command is executed.--- Clients keep a subset of atomic commands sent by the server--- and add their own. The result of this function is the list of commands--- kept for each command received. cmdAtomicSemCli :: MonadClient m => CmdAtomic -> m () cmdAtomicSemCli cmd = case cmd of- DestroyActorA aid _ _ -> destroyActorA aid- LoseActorA aid _ _ -> destroyActorA aid+ CreateActorA aid body _ -> createActorA aid body+ DestroyActorA aid b _ -> destroyActorA aid b True+ SpotActorA aid body _ -> createActorA aid body+ LoseActorA aid b _ -> destroyActorA aid b False LeadFactionA fid source target -> do side <- getsClient sside when (side == fid) $ do@@ -168,66 +215,66 @@ modifyClient $ \cli -> cli {_sleader = target} DiscoverA lid p iid ik -> discoverA lid p iid ik CoverA lid p iid ik -> coverA lid p iid ik- PerceptionA lid outPA inPA -> perceptionA lid outPA inPA- RestartA _ sdisco sfper s sdebugCli _ -> do- side <- getsClient sside- let fact = sfactionD s EM.! side+ PerceptionA lid outPer inPer -> perceptionA lid outPer inPer+ RestartA side sdisco sfper _ sdebugCli _ -> do shistory <- getsClient shistory sconfigUI <- getsClient sconfigUI isAI <- getsClient sisAI let cli = defStateClient shistory sconfigUI side isAI putClient cli { sdisco , sfper- , _sleader = gleader fact- , sundo = [CmdAtomic cmd]- , sdebugCli}+ -- , sundo = [CmdAtomic cmd]+ , scurDifficulty = sdifficultyCli sdebugCli+ , sdebugCli } ResumeA _fid sfper -> modifyClient $ \cli -> cli {sfper} KillExitA _fid -> killExitA SaveBkpA -> saveClient _ -> return () --- We wipe out the target, because if the actor gets recreated--- on another level, the old position does not make sense.--- And if he is really dead, we avoid a memory leak.-destroyActorA :: MonadClient m => ActorId -> m ()-destroyActorA aid =- modifyClient $ \cli -> cli {stargetD = EM.delete aid $ stargetD cli}+createActorA :: MonadClient m => ActorId -> Actor -> m ()+createActorA aid _b = do+ let affect tgt = case tgt of+ TEnemyPos a _ _ permit | a == aid -> TEnemy a permit+ _ -> tgt+ affect3 (tgt, mpath) = case tgt of+ TEnemyPos a _ _ permit | a == aid -> (TEnemy a permit, Nothing)+ _ -> (tgt, mpath)+ modifyClient $ \cli -> cli {stargetD = EM.map affect3 (stargetD cli)}+ modifyClient $ \cli -> cli {scursor = affect $ scursor cli} -perceptionA :: MonadClient m => LevelId -> PerActor -> PerActor -> m ()-perceptionA lid outPA inPA = do- cops <- getsState scops- s <- getState+destroyActorA :: MonadClient m => ActorId -> Actor -> Bool -> m ()+destroyActorA aid b destroy = do+ when destroy $ modifyClient $ updateTarget aid (const Nothing) -- gc+ modifyClient $ \cli -> cli {sbfsD = EM.delete aid $ sbfsD cli} -- gc+ let affect tgt = case tgt of+ TEnemy a permit | a == aid -> TEnemyPos a (blid b) (bpos b) permit+ -- Don't consider @destroy@, because even if actor dead, it makes+ -- sense to go to last known location to loot or find others.+ _ -> tgt+ affect3 (tgt, mpath) = (affect tgt, mpath) -- old path always good+ modifyClient $ \cli -> cli {stargetD = EM.map affect3 (stargetD cli)}+ modifyClient $ \cli -> cli {scursor = affect $ scursor cli}++perceptionA :: MonadClient m => LevelId -> Perception -> Perception -> m ()+perceptionA lid outPer inPer = do -- Clients can't compute FOV on their own, because they don't know -- if unknown tiles are clear or not. Server would need to send -- info about properties of unknown tiles, which complicates -- and makes heavier the most bulky data set in the game: tile maps.- -- Note we assume, but do not check that @outPA@ is contained- -- in current perception and @inPA@ has no common part with it.+ -- Note we assume, but do not check that @outPer@ is contained+ -- in current perception and @inPer@ has no common part with it. -- It would make the already very costly operation even more expensive. perOld <- getPerFid lid -- Check if new perception is already set in @cmdAtomicFilterCli@ -- or if we are doing undo/redo, which does not involve filtering. -- The data structure is strict, so the cheap check can't be any simpler.- let interHead [] = Nothing- interHead ((aid, vis) : _) =- Just $ pvisible vis `ES.intersection`- maybe ES.empty pvisible (EM.lookup aid (perActor perOld))- unset = maybe False ES.null (interHead (EM.assocs inPA))- || maybe False (not . ES.null) (interHead (EM.assocs outPA))+ let interAlready per =+ Just $ totalVisible per `ES.intersection` totalVisible perOld+ unset = maybe False ES.null (interAlready inPer)+ || maybe False (not . ES.null) (interAlready outPer) when unset $ do- let dummyToPer Perception{perActor} = Perception- { perActor- , ptotal = PerceptionVisible- $ ES.unions $ map pvisible $ EM.elems perActor- , psmell = smellFromActors cops s perActor }- paToDummy perActor = Perception- { perActor- , ptotal = PerceptionVisible ES.empty- , psmell = PerceptionVisible ES.empty }- outPer = paToDummy outPA- inPer = paToDummy inPA- adj Nothing = assert `failure` "no perception to alter" `twith` lid- adj (Just per) = Just $ dummyToPer $ addPer (diffPer per outPer) inPer+ let adj Nothing = assert `failure` "no perception to alter" `twith` lid+ adj (Just per) = Just $ addPer (diffPer per outPer) inPer f = EM.alter adj lid modifyClient $ \cli -> cli {sfper = f (sfper cli)} @@ -268,23 +315,43 @@ -- the client state is modified by the command. drawCmdAtomicUI :: MonadClientUI m => Bool -> CmdAtomic -> m () drawCmdAtomicUI verbose cmd = case cmd of- CreateActorA aid body _ -> do- when verbose $ actorVerbMU aid body "appear"- lookAtMove aid- DestroyActorA aid body _ ->+ CreateActorA aid body _ -> createActorUI aid body verbose "appear"+ DestroyActorA aid body _ -> do destroyActorUI aid body "die" "be destroyed" verbose+ side <- getsClient sside+ when (bfid body == side && not (bproj body)) stopPlayBack CreateItemA _ item k _ -> itemVerbMU item k "drop to the ground" DestroyItemA _ item k _ -> itemVerbMU item k "disappear"+ SpotActorA aid body _ -> createActorUI aid body verbose "be spotted" LoseActorA aid body _ -> destroyActorUI aid body "be missing in action" "be lost" verbose+ SpotItemA _ item k c -> do+ scursorOld <- getsClient scursor+ case scursorOld of+ TEnemy{} -> return () -- probably too important to overwrite+ TEnemyPos{} -> return ()+ _ -> do+ (lid, p) <- posOfContainer c+ modifyClient $ \cli -> cli {scursor = TPoint lid p}+ stopPlayBack+ -- TODO: perhaps don't spam for already seen items; very hard to do+ itemVerbMU item k "be spotted" MoveActorA aid _ _ -> lookAtMove aid WaitActorA aid _ _| verbose -> aVerbMU aid "wait" DisplaceActorA source target -> displaceActorUI source target MoveItemA iid k c1 c2 -> moveItemUI verbose iid k c1 c2- HealActorA aid n | verbose ->- aVerbMU aid $ MU.Text $ if n > 0- then "heal" <+> showT n <> "HP"- else "be about to lose" <+> showT n <> "HP"+ HealActorA aid n -> do+ when verbose $+ aVerbMU aid $ MU.Text $ (if n > 0 then "heal" else "lose")+ <+> tshow (abs n) <> "HP"+ mleader <- getsClient _sleader+ when (Just aid == mleader) $ do+ Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ b <- getsState $ getActorBody aid+ let ActorKind{ahp} = okind $ bkind b+ when (bhp b == maxDice ahp) $ do+ actorVerbMU aid b "heal fully"+ stopPlayBack HasteActorA aid delta -> aVerbMU aid $ if delta > speedZero then "speed up"@@ -313,13 +380,18 @@ QuitFactionA fid mbody _ toSt -> quitFactionUI fid mbody toSt AlterTileA{} | verbose -> return () -- TODO: door opens- SearchTileA aid _ fromTile toTile -> do+ SearchTileA aid p fromTile toTile -> do Kind.COps{cotile = Kind.Ops{okind}} <- getsState scops+ b <- getsState $ getActorBody aid+ lvl <- getLevel $ blid b subject <- partAidLeader aid- let verb = "reveal that the"+ let t = lvl `at` p+ verb | t == toTile = "confirm"+ | otherwise = "reveal" subject2 = MU.Text $ tname $ okind fromTile verb2 = "be" let msg = makeSentence [ MU.SubjectVerbSg subject verb+ , "that the" , MU.SubjectVerbSg subject2 verb2 , "a hidden" , MU.Text $ tname $ okind toTile ]@@ -357,8 +429,7 @@ "look like an ordinary" , objUnkown1, objUnkown2 ] msgAdd msg- RestartA _ _ _ _ _ t ->- msgAdd $ "New game started in" <+> t <+> "mode."+ RestartA _ _ _ _ _ t -> msgAdd $ "New game started in" <+> t <+> "mode." SaveBkpA | verbose -> msgAdd "Saving backup." MsgAllA msg -> msgAdd msg _ -> return ()@@ -371,8 +442,16 @@ when (not (bproj body) && bfid body == side && isNothing tgtMode) $ do -- targeting does a more extensive look- lookMsg <- lookAt False True (bpos body) aid ""+ lookMsg <- lookAt False "" True (bpos body) aid "" msgAdd lookMsg+ fact <- getsState $ (EM.! bfid body) . sfactionD+ Level{lxsize, lysize} <- getsState $ (EM.! blid body) . sdungeon+ if side == bfid body then do+ foes <- getsState $ actorList (isAtWar fact) (blid body)+ when (foesAdjacent lxsize lysize (bpos body) foes) stopPlayBack+ else when (isAtWar fact side) $ do+ foes <- getsState $ actorNotProjList (== side) (blid body)+ when (foesAdjacent lxsize lysize (bpos body) foes) stopPlayBack -- | Sentences such as \"Dog barks loudly.\". actorVerbMU :: MonadClientUI m => ActorId -> Actor -> MU.Part -> m ()@@ -409,6 +488,23 @@ , partItemWs coitem disco k item ] msgAdd msg +-- TODO: "XXX spots YYY"? or blink or show the changed cursor?+createActorUI :: MonadClientUI m => ActorId -> Actor -> Bool -> MU.Part -> m ()+createActorUI aid body verbose verb = do+ side <- getsClient sside+ when (verbose || bfid body /= side) $ actorVerbMU aid body verb+ when (bfid body /= side) $ do+ fact <- getsState $ (EM.! bfid body) . sfactionD+ when (not (bproj body) && isAtWar fact side) $ do+ -- Target even if nobody can aim at the enemy. Let's home in on him+ -- and then we can aim or melee. We set permit to False, because it's+ -- technically very hard to check aimability here, because we are+ -- in-between turns and, e.g., leader's move has not yet been taken+ -- into account.+ modifyClient $ \cli -> cli {scursor = TEnemy aid False}+ stopPlayBack+ lookAtMove aid+ destroyActorUI :: MonadClientUI m => ActorId -> Actor -> MU.Part -> MU.Part -> Bool -> m () destroyActorUI aid body verb verboseVerb verbose = do@@ -457,18 +553,16 @@ quitFactionUI :: MonadClientUI m => FactionId -> Maybe Actor -> Maybe Status -> m () quitFactionUI fid mbody toSt = do- Kind.COps{coitem=Kind.Ops{okind, ouniqGroup}} <- getsState scops- factionD <- getsState sfactionD- let fact = factionD EM.! fid- fidName = MU.Text $ gname fact+ cops@Kind.COps{coitem=Kind.Ops{okind, ouniqGroup}} <- getsState scops+ fact <- getsState $ (EM.! fid) . sfactionD+ let fidName = MU.Text $ gname fact+ horror = isHorrorFact cops fact side <- getsClient sside- spawn <- getsState $ isSpawnFaction fid- summon <- getsState $ isSummonFaction fid let msgIfSide _ | fid /= side = Nothing msgIfSide s = Just s (startingPart, partingPart) = case toSt of- _ | summon && not spawn ->- (Nothing, Nothing) -- Ignore summoned actors factions.+ _ | horror ->+ (Nothing, Nothing) -- Ignore summoned actors' factions. Just Status{stOutcome=Killed} -> ( Just "be eliminated" , msgIfSide "Let's hope another party can save the day!" )@@ -514,7 +608,7 @@ startingSlide <- promptToSlideshow moreMsg recordHistory -- we are going to exit or restart, so record itemSlides <-- if EM.null bag then return Monoid.mempty+ if EM.null bag then return mempty else do io <- floorItemOverlay bag overlayToSlideshow itemMsg io@@ -525,10 +619,10 @@ shutdownSlide <- promptToSlideshow pp -- TODO: First ESC cancels items display. void $ getInitConfirms ColorFull []- $ startingSlide Monoid.<> itemSlides+ $ startingSlide <> itemSlides -- TODO: Second ESC cancels high score and parting message display. -- The last slide stays onscreen during shutdown, etc.- Monoid.<> scoreSlides Monoid.<> partingSlide Monoid.<> shutdownSlide+ <> scoreSlides <> partingSlide <> shutdownSlide _ -> return () -- * SfxAtomicUI
Game/LambdaHack/Client/Binding.hs view
@@ -1,68 +1,60 @@--- | Generic binding of keys to commands, procesing macros,--- printing command help. No operation in this module--- involves the 'State' or 'Action' type.+-- | Binding of keys to commands.+-- No operation in this module involves the 'State' or 'Action' type. module Game.LambdaHack.Client.Binding ( Binding(..), stdBinding, keyHelp, ) where import Control.Arrow (second) import qualified Data.Char as Char-import qualified Data.List as L+import Data.List import qualified Data.Map.Strict as M-import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Tuple (swap) import Game.LambdaHack.Client.Config-import Game.LambdaHack.Client.HumanCmd+import Game.LambdaHack.Common.HumanCmd import qualified Game.LambdaHack.Common.Key as K+import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Content.RuleKind -- | Bindings and other information about human player commands. data Binding = Binding- { kcmd :: !(M.Map K.KM (Text, Bool, HumanCmd))- -- ^ binding keys to commands- , kmacro :: !(M.Map K.KM K.KM) -- ^ macro map- , kmajor :: ![K.KM] -- ^ major commands- , kminor :: ![K.KM] -- ^ minor commands- , krevMap :: !(M.Map HumanCmd K.KM) -- ^ from cmds to their main keys+ { bcmdMap :: !(M.Map K.KM (Text, CmdCategory, HumanCmd))+ -- ^ binding of keys to commands+ , bcmdList :: ![(K.KM, (Text, CmdCategory, HumanCmd))]+ -- ^ the properly ordered list+ -- of commands for the help menu+ , brevMap :: !(M.Map HumanCmd K.KM) -- ^ and from commands to their keys } -- | Binding of keys to movement and other standard commands, -- as well as commands defined in the config file.-stdBinding :: ConfigUI -- ^ game config- -> Binding -- ^ concrete binding-stdBinding !config@ConfigUI{configMacros} =- let kmacro = M.fromList configMacros+stdBinding :: Kind.Ops RuleKind -- ^ default game rules+ -> ConfigUI -- ^ game config+ -> Binding -- ^ concrete binding+stdBinding corule !ConfigUI{configCommands} =+ let stdRuleset = Kind.stdRuleset corule heroSelect k = ( K.KM { key=K.Char (Char.intToDigit k) , modifier=K.NoModifier }- , SelectHero k )- cmdList =- configCommands config- ++ K.moveBinding Move Run+ , (CmdMeta, PickLeader k) )+ cmdWithHelp = rhumanCommands stdRuleset ++ configCommands+ cmdAll =+ cmdWithHelp+ ++ [(K.mkKM "KP_Begin", (CmdMove, Wait))]+ ++ K.moveBinding (\v -> (CmdMove, Move v)) (\v -> (CmdMove, Run v)) ++ fmap heroSelect [0..9]- mkDescribed cmd = (cmdDescription cmd, noRemoteHumanCmd cmd, cmd)- mkCommand = second mkDescribed- semList = L.map mkCommand cmdList+ mkDescribed (cat, cmd) = (cmdDescription cmd, cat, cmd) in Binding- { kcmd = M.fromList semList- , kmacro- , kmajor = L.map fst $ L.filter (majorHumanCmd . snd) cmdList- , kminor = L.map fst $ L.filter (minorHumanCmd . snd) cmdList- , krevMap = M.fromList $ map swap cmdList+ { bcmdMap = M.fromList $ map (second mkDescribed) cmdAll+ , bcmdList = map (second mkDescribed) cmdWithHelp+ , brevMap = M.fromList $ map swap $ map (second snd) cmdAll } -coImage :: M.Map K.KM K.KM -> K.KM -> [K.KM]-coImage kmacro k =- let domain = M.keysSet kmacro- in if k `S.member` domain- then []- else k : [ from | (from, to) <- M.assocs kmacro, to == k ]- -- | Produce a set of help screens from the key bindings. keyHelp :: Binding -> Slideshow-keyHelp Binding{kcmd, kmacro, kmajor, kminor} =+keyHelp Binding{bcmdList} = let movBlurb = [ "Move throughout the level with numerical keypad or"@@ -74,45 +66,47 @@ , " /|\\ /|\\" , " 1 2 3 b j n" , ""- ,"Run ahead until anything disturbs you, with SHIFT (or CTRL) and a key."+ , "Run ahead, until anything disturbs you, with SHIFT (or CTRL) and a key." , "Press keypad '5' or '.' to wait a turn, bracing for blows next turn." , "In targeting mode the same keys move the targeting cursor." , "" , "Search, open and attack, by bumping into walls, doors and enemies." , ""- , "Press SPACE to see the next page, with the list of major commands."+ , "Press SPACE to see detailed command descriptions." ]- majorBlurb =+ categoryBlurb = [ ""- , "Commands marked with * take time and are blocked on remote levels."- , "Press SPACE to see the next page, with the list of minor commands."+ , "Press SPACE to see the next page of command descriptions." ]- minorBlurb =+ lastBlurb = [ "" , "For more playing instructions see file PLAYING.md." , "Press SPACE to clear the messages and see the map again." ]- 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+ fmt k h = T.justifyRight 72 ' '+ $ T.justifyLeft 15 ' ' k+ <> T.justifyLeft 41 ' ' h+ fmts s = " " <> T.justifyLeft 71 ' ' s+ movText = map fmts movBlurb+ categoryText = map fmts categoryBlurb+ lastText = map fmts lastBlurb keyCaption = fmt "keys" "command"- disp k = T.concat $ map K.showKM $ coImage kmacro k- keys l = [ fmt (disp k) (h <> if timed then "*" else "")- | (k, (h, timed, _)) <- l, h /= "" ]- (kcMajor, kcRest) =- L.partition ((`elem` kmajor) . fst) (M.toAscList kcmd)- (kcMinor, _) =- L.partition ((`elem` kminor) . fst) kcRest- in toSlideshow- [ ["Basic keys. [press SPACE to advance]"] ++ [blank]- ++ mov ++ [moreMsg]- , ["Basic keys. [press SPACE to advance]"] ++ [blank]- ++ [keyCaption] ++ keys kcMajor ++ major ++ [moreMsg]- , ["Basic keys."] ++ [blank]- ++ [keyCaption] ++ keys kcMinor ++ minor+ coImage :: K.KM -> [K.KM]+ coImage k = k : sort [ from+ | (from, (_, _, Macro _ [to])) <- bcmdList+ , K.mkKM to == k ]+ disp k = T.concat $ intersperse " and " $ map K.showKM $ coImage k+ keys cat = [ fmt (disp k) h+ | (k, (h, cat', _)) <- bcmdList, cat == cat', h /= "" ]+ in toSlideshow True+ [ ["Basic keys. [press SPACE to advance]"] ++ [""]+ ++ movText ++ [moreMsg]+ , [categoryDescription CmdMove <> ". [press SPACE to advance]"] ++ [""]+ ++ [keyCaption] ++ keys CmdMove ++ categoryText ++ [moreMsg]+ , [categoryDescription CmdItem <> ". [press SPACE to advance]"] ++ [""]+ ++ [keyCaption] ++ keys CmdItem ++ categoryText ++ [moreMsg]+ , [categoryDescription CmdTgt <> ". [press SPACE to advance]"] ++ [""]+ ++ [keyCaption] ++ keys CmdTgt ++ categoryText ++ [moreMsg]+ , [categoryDescription CmdMeta <> "."] ++ [""]+ ++ [keyCaption] ++ keys CmdMeta ++ lastText ]
Game/LambdaHack/Client/ClientSem.hs view
@@ -1,255 +1,325 @@ -- | Semantics of most 'CmdClientAI' client commands. module Game.LambdaHack.Client.ClientSem where +import Control.Exception.Assert.Sugar import Control.Monad-import Control.Monad.Writer.Strict (runWriterT) import qualified Data.EnumMap.Strict as EM+import Data.List import qualified Data.Map.Strict as M import Data.Maybe+import Data.Ord import qualified Data.Text as T import Game.LambdaHack.Client.Action import Game.LambdaHack.Client.Binding+import Game.LambdaHack.Client.Config import Game.LambdaHack.Client.Draw-import Game.LambdaHack.Client.HumanCmd-import Game.LambdaHack.Client.HumanLocal import Game.LambdaHack.Client.HumanSem import Game.LambdaHack.Client.RunAction import Game.LambdaHack.Client.State import Game.LambdaHack.Client.Strategy import Game.LambdaHack.Client.StrategyAction-import qualified Game.LambdaHack.Common.Ability as Ability import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction-import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.HumanCmd import qualified Game.LambdaHack.Common.Key as K import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Msg-import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.ServerCmd import Game.LambdaHack.Common.State+import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.FactionKind-import Game.LambdaHack.Content.RuleKind-import Control.Exception.Assert.Sugar import Game.LambdaHack.Utils.Frequency -queryAI :: MonadClient m => ActorId -> m CmdSerTakeTime+queryAI :: MonadClient m => ActorId -> m CmdTakeTimeSer queryAI oldAid = do- Kind.COps{cofaction=Kind.Ops{okind}, corule} <- getsState scops- side <- getsClient sside- fact <- getsState $ \s -> sfactionD s EM.! side- let abilityLeader = fAbilityLeader $ okind $ gkind fact+ Kind.COps{cotile, cofaction=Kind.Ops{okind}} <- getsState scops+ oldBody <- getsState $ getActorBody oldAid+ let side = bfid oldBody+ arena = blid oldBody+ fact <- getsState $ (EM.! side) . sfactionD+ lvl <- getLevel arena+ let leaderStuck = waitedLastTurn oldBody+ t = lvl `at` bpos oldBody+ abilityLeader = fAbilityLeader $ okind $ gkind fact abilityOther = fAbilityOther $ okind $ gkind fact mleader <- getsClient _sleader- if -- Keep the leader: only a leader is allowed to pick another leader.- mleader /= Just oldAid- -- Keep the leader: abilities are the same (we assume leader can do- -- at least as much as others).- || abilityLeader == abilityOther- -- Keep the leader: other members can't melee.- || Ability.Melee `notElem` abilityOther- then queryAIPick oldAid- else do- fper <- getsClient sfper- oldBody <- getsState $ getActorBody oldAid- oldAis <- getsState $ getActorItem oldAid- btarget <- getsClient $ getTarget oldAid- let arena = blid oldBody- -- Visibility ignored --- every foe is visible by somebody.- foes <- getsState $ actorNotProjList (isAtWar fact) arena- ours <- getsState $ actorNotProjAssocs (== side) arena- time <- getsState $ getLocalTime arena- Level{lxsize, lysize} <- getsState $ \s -> sdungeon s EM.! arena- actorD <- getsState sactorD- let oldPos = bpos oldBody- per = fper EM.! arena- posOnLevel b | blid b /= arena = Nothing- | otherwise = Just $ bpos b- mfoePos foe = maybe Nothing posOnLevel- $ EM.lookup foe actorD- canSee foe = maybe False (actorSeesPos per oldAid) $ mfoePos foe- isAmmo i = jsymbol i `elem` ritemProject (Kind.stdRuleset corule)- hasAmmo = any (isAmmo . snd) oldAis- isAdjacent = foesAdjacent lxsize lysize oldPos foes- if -- Keep the leader: he is alone on the level.- length ours == 1- -- Keep the leader: he has an enemy target.- || case btarget of- Just (TEnemy foe _) ->- -- and he can shoot it.- canSee foe && hasAmmo && not isAdjacent- _ -> False- -- Keep the leader: he probably just used stairs.- || bpos oldBody == boldpos oldBody- && not (waitedLastTurn oldBody time)- then queryAIPick oldAid- else do- let countMinFoeDist (aid, b) =- let distB = chessDist lxsize (bpos b)- foeDist = map (distB . bpos) foes- minFoeDist | null foeDist = maxBound- | otherwise = minimum foeDist- in ((aid, b), minFoeDist)- oursMinFoeDist = map countMinFoeDist ours- inMelee (_, minFoeDist) = minFoeDist == 1- oursMeleePos = map (bpos . snd . fst)- $ filter inMelee oursMinFoeDist- let f ((aid, b), minFoeDist) =- let distB = chessDist lxsize (bpos b)- meleeDist = map distB oursMeleePos- minMeleeDist | null meleeDist = maxBound- | otherwise = minimum meleeDist- proximityMelee = max 0 $ 10 - minMeleeDist- proximityFoe = max 0 $ 20 - minFoeDist- distToLeader = distB oldPos- proximityLeader = max 0 $ 10 - distToLeader- in if minFoeDist == 1- || bhp b <= 0- || aid == oldAid && waitedLastTurn b time- then -- Ignore: in melee range or incapacitated or stuck.- Nothing- else -- Help in melee, shoot or chase foes,- -- fan out away from each other, if too close.- Just ( 1- + proximityMelee * 9- + proximityFoe * 6- + proximityLeader * 3- , aid )- candidates = mapMaybe f oursMinFoeDist- freq | null candidates = toFreq "old leader" [(1, oldAid)]- | otherwise = toFreq "candidates for AI leader" candidates- aid <- rndToAction $ frequency freq+ ours <- getsState $ actorNotProjAssocs (== side) arena+ let pickOld = do+ void $ refreshTarget oldAid (oldAid, oldBody)+ queryAIPick (oldAid, oldBody)+ case ours of+ _ | -- Keep the leader: only a leader is allowed to pick another leader.+ mleader /= Just oldAid+ -- Keep the leader: abilities are the same (we assume leader can do+ -- at least as much as others). TODO: check not accurate,+ -- instead define 'movesThisTurn' and use elsehwere.+ || abilityLeader == abilityOther+ -- Keep the leader: spawners can't change leaders themselves.+ || isSpawnFact fact+ -- Keep the leader: he is on stairs and not stuck+ -- and we don't want to clog stairs or get pushed to another level.+ || not leaderStuck && Tile.isStair cotile t+ -> pickOld+ [] -> assert `failure` (oldAid, oldBody)+ [_] -> pickOld -- Keep the leader: he is alone on the level.+ (captain, captainBody) : (sergeant, sergeantBody) : _ -> do+ -- At this point we almost forget who the old leader was+ -- and treat all party actors the same, eliminating candidates+ -- until we can't distinguish them any more, at which point we prefer+ -- the old leader, if he is among the best candidates+ -- (to make the AI appear more human-like to easier to observe).+ -- TODO: this also takes melee into account, not shooting.+ oursTgt <- fmap catMaybes $ mapM (refreshTarget oldAid) ours+ let targetTEnemy (_, (TEnemy{}, _)) = True+ targetTEnemy _ = False+ (oursTEnemy, oursOther) = partition targetTEnemy oursTgt+ -- These are not necessarily stuck (perhaps can go around),+ -- but their current path is blocked by friends.+ targetBlocked our@((_aid, _b), (_tgt, (path, _etc))) =+ let next = case path of+ [] -> assert `failure` our+ [_goal] -> Nothing+ _ : q : _ -> Just q+ in any ((== next) . Just . bpos . snd) ours+-- TODO: stuck actors are picked while others close could approach an enemy;+-- we should detect stuck actors (or one-sided stuck)+-- so far we only detect blocked and only in Other mode+-- && not (aid == oldAid && waitedLastTurn b time) -- not stuck+-- this only prevents staying stuck+ (oursBlocked, oursPos) = partition targetBlocked oursOther+ valueOurs :: ((ActorId, Actor), (Target, PathEtc))+ -> (Int, Int, Bool)+ valueOurs our@((aid, b), (TEnemy{}, (_, (_, d)))) =+ -- TODO: take weapon, walk and fight speed, etc. into account+ ( d + if targetBlocked our then 2 else 0 -- possible delay, hacky+ , - 10 * (bhp b `div` 10)+ , aid /= oldAid )+ valueOurs ((aid, b), (_tgt, (_path, (goal, d)))) =+ -- Keep proper formation, not too dense, not to sparse.+ let -- TODO: vary the parameters according to the stage of game,+ -- enough equipment or not, game mode, level map, etc.+ minSpread = 7+ maxSpread = 12 * 2+ dcaptain p =+ chessDistVector $ displacement p (bpos captainBody)+ dsergeant p =+ chessDistVector $ displacement p (bpos sergeantBody)+ minDist | aid == captain = dsergeant (bpos b)+ | aid == sergeant = dcaptain (bpos b)+ | otherwise = dsergeant (bpos b)+ `min` dcaptain (bpos b)+ pDist p = dcaptain p + dsergeant p+ sumDist = pDist (bpos b)+ -- Positive, if the goal gets us closer to the party.+ diffDist = sumDist - pDist goal+ minCoeff | minDist < minSpread =+ (minDist - minSpread) `div` 3+ - if aid == oldAid then 3 else 0+ | otherwise = 0+ explorationValue = diffDist * (sumDist `div` 4)+-- TODO: this half is not yet ready:+-- instead spread targets between actors; moving many actors+-- to a single target and stopping and starting them+-- is very wasteful; also, pick targets not closest to the actor in hand,+-- but to the sum of captain and sergant or something+ sumCoeff | sumDist > maxSpread = - explorationValue+ | otherwise = 0+ in ( if d == 0 then d+ else max 1 $ minCoeff + if d < 10+ then 3 + d `div` 4+ else 9 + d `div` 10+ , sumCoeff+ , aid /= oldAid )+ sortOurs = sortBy $ comparing valueOurs+ goodGeneric _our@((aid, b), (_tgt, _pathEtc)) =+ bhp b > 0 -- not incapacitated+ && not (aid == oldAid && waitedLastTurn b) -- not stuck+ goodTEnemy our@((_aid, b), (_tgt, (_path, (goal, _d)))) =+ not (adjacent (bpos b) goal) -- not in melee range already+ && goodGeneric our+ oursTEnemyGood = filter goodTEnemy oursTEnemy+ oursPosGood = filter goodGeneric oursPos+ oursBlockedGood = filter goodGeneric oursBlocked+ candidates = sortOurs oursTEnemyGood+ ++ sortOurs oursPosGood+ ++ sortOurs oursBlockedGood+ case candidates of+ [] -> queryAIPick (oldAid, oldBody)+ c : _ -> do+ let best = takeWhile ((== valueOurs c) . valueOurs) candidates+ freq = uniformFreq "candidates for AI leader" best+ ((aid, b), _) <- rndToAction $ frequency freq s <- getState modifyClient $ updateLeader aid s- queryAIPick aid+ queryAIPick (aid, b) -queryAIPick :: MonadClient m => ActorId -> m CmdSerTakeTime-queryAIPick aid = do- Kind.COps{cofaction=Kind.Ops{okind}} <- getsState scops+refreshTarget :: MonadClient m+ => ActorId -> (ActorId, Actor)+ -> m (Maybe ((ActorId, Actor), (Target, PathEtc)))+refreshTarget oldLeader (aid, body) = do side <- getsClient sside- body <- getsState $ getActorBody aid+ assert (bfid body == side `blame` "AI tries to move an enemy actor"+ `twith` (aid, body, side)) skip+ assert (not (bproj body) `blame` "AI gets to manually move its projectiles"+ `twith` (aid, body, side)) skip+ stratTarget <- targetStrategy oldLeader aid+ tgtMPath <-+ if nullStrategy stratTarget then+ -- No sensible target, wipe out the old one .+ return Nothing+ else do+ -- Choose a target from those proposed by AI for the actor.+ (tgt, path) <- rndToAction $ frequency $ bestVariant stratTarget+ return $ Just (tgt, Just path)+ let _debug = T.unpack+ $ "\nHandleAI symbol:" <+> tshow (bsymbol body)+ <> ", aid:" <+> tshow aid+ <> ", pos:" <+> tshow (bpos body)+ <> "\nHandleAI starget:" <+> tshow stratTarget+ <> "\nHandleAI target:" <+> tshow tgtMPath+-- trace _debug skip+ modifyClient $ \cli ->+ cli {stargetD = EM.alter (const $ tgtMPath) aid (stargetD cli)}+ return $! case tgtMPath of+ Just (tgt, Just pathEtc) -> Just ((aid, body), (tgt, pathEtc))+ _ -> Nothing++queryAIPick :: MonadClient m => (ActorId, Actor) -> m CmdTakeTimeSer+queryAIPick (aid, body) = do+ side <- getsClient sside assert (bfid body == side `blame` "AI tries to move enemy actor" `twith` (aid, bfid body, side)) skip- mleader <- getsClient _sleader- fact <- getsState $ (EM.! bfid body) . sfactionD- let factionAbilities- | Just aid == mleader = fAbilityLeader $ okind $ gkind fact- | otherwise = fAbilityOther $ okind $ gkind fact- unless (bproj body) $ do- stratTarget <- targetStrategy aid factionAbilities- -- Choose a target from those proposed by AI for the actor.- btarget <- rndToAction $ frequency $ bestVariant stratTarget- let _debug = T.unpack- $ "\nHandleAI abilities:" <+> showT factionAbilities- <> ", symbol:" <+> showT (bsymbol body)- <> ", aid:" <+> showT aid- <> ", pos:" <+> showT (bpos body)- <> "\nHandleAI starget:" <+> showT stratTarget- <> "\nHandleAI target:" <+> showT btarget--- trace _debug skip- modifyClient $ updateTarget aid (const btarget)- stratAction <- actionStrategy aid factionAbilities+ assert (not (bproj body) `blame` "AI gets to manually move its projectiles"+ `twith` (aid, bfid body, side)) skip+ stratAction <- actionStrategy aid -- Run the AI: chose an action from those given by the AI strategy.- action <- rndToAction $ frequency $ bestVariant stratAction- let _debug = T.unpack- $ "HandleAI saction:" <+> showT stratAction- <> "\nHandleAI action:" <+> showT action--- trace _debug skip- return action+ rndToAction $ frequency $ bestVariant stratAction -- | Handle the move of a UI player.-queryUI :: (MonadClientAbort m, MonadClientUI m) => ActorId -> m CmdSer+queryUI :: MonadClientUI m => ActorId -> m CmdSer queryUI aid = do- -- When running, stop if aborted by a disturbance. Otherwise let- -- the human player issue commands, until any of them takes time.+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ -- When running, stop if disturbed. If not running, let the human+ -- player issue commands, until any command takes time. leader <- getLeaderUI assert (leader == aid `blame` "player moves not his leader" `twith` (leader, aid)) skip- let inputHumanCmd msg = do- stopRunning- humanCommand msg- tryWith inputHumanCmd $ do- srunning <- getsClient srunning- maybe abort (continueRun leader) srunning---- | Continue running in the given direction.-continueRun :: MonadClientAbort m => ActorId -> (Vector, Int) -> m CmdSer-continueRun leader dd = do- (dir, distNew) <- continueRunDir leader dd- modifyClient $ \cli -> cli {srunning = Just (dir, distNew)}- -- The potential invisible actor is hit. War is started without asking.- return $ TakeTimeSer $ MoveSer leader dir+ srunning <- getsClient srunning+ case srunning of+ Nothing -> humanCommand Nothing+ Just RunParams{runMembers} | isSpawnFact fact && runMembers /= [aid] -> do+ stopRunning+ ConfigUI{configRunStopMsgs} <- getsClient sconfigUI+ let msg = if configRunStopMsgs+ then Just $ "Run stop: spawner leader change"+ else Nothing+ humanCommand msg+ Just runParams -> do+ runOutcome <- continueRun runParams+ case runOutcome of+ Left stopMsg -> do+ stopRunning+ ConfigUI{configRunStopMsgs} <- getsClient sconfigUI+ let msg = if configRunStopMsgs+ then Just $ "Run stop:" <+> stopMsg+ else Nothing+ humanCommand msg+ Right (paramNew, runCmd) -> do+ modifyClient $ \cli -> cli {srunning = Just paramNew}+ return $! CmdTakeTimeSer runCmd -- | Determine and process the next human player command. The argument is--- the last abort message due to running, if any.-humanCommand :: forall m. (MonadClientAbort m, MonadClientUI m)- => Msg+-- the last stop message due to running, if any.+humanCommand :: forall m. MonadClientUI m+ => Maybe Msg -> m CmdSer-humanCommand msgRunAbort = do- let loop :: Overlay -> m CmdSer- loop overlay = do- km <- getKeyOverlayCommand overlay+humanCommand msgRunStop = do+ -- For human UI we invalidate whole @sbfsD@ at the start of each+ -- UI player input, which is an overkill, but doesn't affects+ -- screensavers, because they are UI, but not human.+ modifyClient $ \cli -> cli {sbfsD = EM.empty}+ let loop :: Maybe (Bool, Overlay) -> m CmdSer+ loop mover = do+ (lastBlank, over) <- case mover of+ Nothing -> do+ -- Display current state if no slideshow or if interrupted.+ modifyClient $ \cli -> cli {slastKey = Nothing}+ sli <- promptToSlideshow ""+ return (False, head . snd $! slideshow sli)+ Just bLast ->+ -- (Re-)display the last slide while waiting for the next key.+ return bLast+ (seqCurrent, seqPrevious, k) <- getsClient slastRecord+ case k of+ 0 -> do+ let slastRecord = ([], seqCurrent, 0)+ modifyClient $ \cli -> cli {slastRecord}+ _ -> do+ let slastRecord = ([], seqCurrent ++ seqPrevious, k - 1)+ modifyClient $ \cli -> cli {slastRecord}+ km <- getKeyOverlayCommand lastBlank over -- Messages shown, so update history and reset current report. recordHistory- -- On abort, just reset state and call loop again below.- -- Each abort that gets this far generates a slide to be shown.- (mcmdS, slides) <- runWriterT $ tryWithSlide (return Nothing) $ do+ abortOrCmd <- do -- Look up the key.- Binding{kcmd} <- askBinding- case M.lookup km kcmd of+ Binding{bcmdMap} <- askBinding+ case M.lookup km bcmdMap of Just (_, _, cmd) -> do -- Query and clear the last command key. lastKey <- getsClient slastKey- -- TODO: perhaps replace slastKey- -- with test 'kmNext == km'- -- or an extra arg to 'loop'.- -- Depends on whether slastKey- -- is needed in other parts of code.- modifyClient (\st -> st {slastKey = Just km})- cmdHumanSem $ if Just km == lastKey- then Clear- else cmd+ stgtMode <- getsClient stgtMode+ modifyClient $ \cli -> cli+ {swaitTimes = if swaitTimes cli > 0+ then - swaitTimes cli+ else 0}+ if Just km == lastKey+ || km == K.escKey && isNothing stgtMode && isJust mover+ then do+ modifyClient $ \cli -> cli {slastKey = Nothing}+ cmdHumanSem Clear+ else do+ modifyClient $ \cli -> cli {slastKey = Just km}+ cmdHumanSem cmd Nothing -> let msgKey = "unknown command <" <> K.showKM km <> ">"- in abortWith msgKey- -- The command was aborted or successful and if the latter,+ in fmap Left $ promptToSlideshow msgKey+ -- The command was failed or successful and if the latter, -- possibly took some time.- case mcmdS of- Just cmdS -> do- assert (null (runSlideshow slides)- `blame` "some slides generated for server command"- `twith` slides) skip+ case abortOrCmd of+ Right cmdS -> do -- Exit the loop and let other actors act. No next key needed -- and no slides could have been generated.- modifyClient (\st -> st {slastKey = Nothing})+ modifyClient $ \cli -> cli {slastKey = Nothing}+ case cmdS of+ CmdTakeTimeSer cmd ->+ modifyClient $ \cli -> cli {slastCmd = Just cmd}+ _ -> return () return cmdS- Nothing -> do+ Left slides -> do -- If no time taken, rinse and repeat. -- Analyse the obtained slides.- mLast <- case reverse (runSlideshow slides) of+ let (onBlank, sli) = slideshow slides+ mLast <- case reverse sli of [] -> return Nothing- [sLast] -> return $ Just sLast+ [sLast] -> return $ Just (onBlank, sLast) sls@(sLast : _) -> do -- Show, one by one, all slides, awaiting confirmation -- for all but the last one. -- Note: the code that generates the slides is responsible -- for inserting the @more@ prompt.- go <- getInitConfirms ColorFull [km] $ toSlideshow $ reverse sls- return $! if go then Just sLast else Nothing- case mLast of- Nothing -> do- -- Display current state if no slideshow or interrupted.- modifyClient (\st -> st {slastKey = Nothing})- sli <- promptToSlideshow ""- loop $! head $! runSlideshow sli- Just sLast ->- -- (Re-)display the last slide while waiting for the next key,- loop sLast- sli <- promptToSlideshow msgRunAbort- let overlayInitial = head $ runSlideshow sli- loop overlayInitial+ go <- getInitConfirms ColorFull [km]+ $ toSlideshow onBlank $ reverse $ map overlay sls+ return $! if go then Just (onBlank, sLast) else Nothing+ loop mLast+ case msgRunStop of+ Nothing -> loop Nothing+ Just msg -> do+ sli <- promptToSlideshow msg+ loop $ Just (False, head . snd $ slideshow sli)
Game/LambdaHack/Client/Config.hs view
@@ -4,51 +4,25 @@ ) where import Control.DeepSeq-import Data.Binary -import Game.LambdaHack.Client.HumanCmd+import Data.Text (Text)+import Game.LambdaHack.Common.HumanCmd import qualified Game.LambdaHack.Common.Key as K -- | Fully typed contents of the UI config file. This config -- is a part of a game client. data ConfigUI = ConfigUI { -- commands- configCommands :: ![(K.KM, HumanCmd)]- -- files- , configAppDataDir :: !FilePath- , configUICfgFile :: !FilePath- , configSavePrefix :: !String- -- macros- , configMacros :: ![(K.KM, K.KM)]+ configCommands :: ![(K.KM, (CmdCategory, HumanCmd))]+ -- hero names+ , configHeroNames :: ![(Int, Text)] -- ui- , configFont :: !String- , configHistoryMax :: !Int- , configMaxFps :: !Int- , configNoAnim :: !Bool+ , configFont :: !String+ , configHistoryMax :: !Int+ , configMaxFps :: !Int+ , configNoAnim :: !Bool+ , configRunStopMsgs :: !Bool } deriving Show instance NFData ConfigUI--instance Binary ConfigUI where- put ConfigUI{..} = do- put configCommands- put configAppDataDir- put configUICfgFile- put configSavePrefix- put configMacros- put configFont- put configHistoryMax- put configMaxFps- put configNoAnim- get = do- configCommands <- get- configAppDataDir <- get- configUICfgFile <- get- configSavePrefix <- get- configMacros <- get- configFont <- get- configHistoryMax <- get- configMaxFps <- get- configNoAnim <- get- return ConfigUI{..}
Game/LambdaHack/Client/Draw.hs view
@@ -6,7 +6,7 @@ import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import qualified Data.List as L+import Data.List import Data.Maybe import Data.Text (Text) import qualified Data.Text as T@@ -14,10 +14,11 @@ import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor as Actor import Game.LambdaHack.Common.ActorState-import Game.LambdaHack.Common.Animation (SingleFrame (..))+import Game.LambdaHack.Common.Animation import qualified Game.LambdaHack.Common.Color as Color import Game.LambdaHack.Common.Effect-import qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Flavour import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Item as Item import qualified Game.LambdaHack.Common.Kind as Kind@@ -25,12 +26,14 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY+import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.State+import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.TileKind -- | Color mode for the display.@@ -39,157 +42,239 @@ | ColorBW -- ^ black+white only -- TODO: split up and generally rewrite.--- | Draw the whole screen: level map, status area and, at most,--- a single page overlay of text divided into lines.-draw :: ColorMode -> Kind.COps -> Perception -> LevelId -> Maybe ActorId- -> StateClient -> State -> Overlay+-- | Draw the whole screen: level map and status area.+-- Pass at most a single page if overlay of text unchanged+-- to the frontends to display separately or overlay over map,+-- depending on the frontend.+draw :: Bool -> ColorMode -> Kind.COps -> Perception -> LevelId+ -> Maybe ActorId -> Maybe Point -> Maybe Point+ -> Maybe (PointArray.Array BfsDistance, Maybe [Point])+ -> StateClient -> State+ -> Text -> Text -> Overlay -> SingleFrame-draw dm cops per drawnLevelId mleader- StateClient{ stgtMode, scursor, seps, sdisco- , smarkVision, smarkSmell, smarkSuspect }- s overlay =- let Kind.COps{cotile=Kind.Ops{okind=tokind, ouniqGroup}} = cops- (lvl@Level{ ldepth, lxsize, lysize, lsmell- , ldesc, ltime, lseen, lclear }) = sdungeon s EM.! drawnLevelId- (msgTop, over, msgBottom) = stringByLocation lxsize lysize overlay- -- TODO:- sVisBG = if smarkVision- then \ vis visPl -> if visPl- then Color.Magenta- else if vis- then Color.Blue- else Color.defBG- else \ _vis _visPl -> Color.defBG- wealth = case mleader of- Nothing -> 0- Just leader -> snd $ calculateTotal (getActorBody leader s) s- bl = case (scursor, mleader) of+draw sfBlank dm cops per drawnLevelId mleader cursorPos tgtPos bfsmpathRaw+ cli@StateClient{ stgtMode, seps, sdisco, sexplored+ , smarkVision, smarkSmell, smarkSuspect, swaitTimes } s+ cursorDesc targetDesc sfTop =+ let Kind.COps{cotile=cotile@Kind.Ops{okind=tokind, ouniqGroup}} = cops+ (lvl@Level{lxsize, lysize, lsmell, ltime}) = sdungeon s EM.! drawnLevelId+ (bl, blLength) = case (cursorPos, mleader) of (Just cursor, Just leader) -> let Actor{bpos, blid} = getActorBody leader s in if blid /= drawnLevelId- then []- else fromMaybe [] $ bla lxsize lysize seps bpos cursor+ then ([cursor], 0)+ else ( fromMaybe [] $ bla lxsize lysize seps bpos cursor+ , chessDist bpos cursor )+ _ -> ([], 0)+ mpath = maybe Nothing (\(_, mp) -> if blLength == 0+ then Nothing+ else mp) bfsmpathRaw+ actorsHere = actorAssocs (const True) drawnLevelId s+ cursorHere = find (\(_, m) -> cursorPos == Just (Actor.bpos m))+ actorsHere+ shiftedBTrajectory = case cursorHere of+ Just (_, Actor{btrajectory = Just p, bpos = prPos}) -> trajectoryToPath prPos p _ -> []- dis pxy =- let pos0 = toPoint lxsize pxy- tile = lvl `at` pos0+ unknownId = ouniqGroup "unknown space"+ dis pos0 =+ let tile = lvl `at` pos0 tk = tokind tile items = lvl `atI` pos0 sml = EM.findWithDefault timeZero pos0 lsmell smlt = sml `timeAdd` timeNegate ltime viewActor aid Actor{bsymbol, bcolor, bhp, bproj}- | Just aid == mleader = (symbol, Color.defBG)- | otherwise = (symbol, color)+ | Just aid == mleader = (symbol, inverseVideo)+ | otherwise = (symbol, Color.defAttr {Color.fg = bcolor}) where- color = bcolor symbol | bhp <= 0 && not bproj = '%' | otherwise = bsymbol- rainbow p = toEnum $ fromEnum p `rem` 14 + 1- actorsHere = actorAssocs (const True) drawnLevelId s+ rainbow p = Color.defAttr {Color.fg =+ toEnum $ fromEnum p `rem` 14 + 1} -- smarkSuspect is an optional overlay, so let's overlay it -- over both visible and invisible tiles.- vcolor t- | smarkSuspect && F.Suspect `elem` tfeature t = Color.BrCyan- | vis = tcolor t- | otherwise = tcolor2 t- (char, fg0) =- case ( L.find (\ (_, m) -> pos0 == Actor.bpos m) actorsHere- , L.find (\ (_, m) -> scursor == Just (Actor.bpos m))- actorsHere ) of- (_, actorTgt) | isJust stgtMode- && (L.elem pos0 bl- || (case actorTgt of- Just (_, Actor{ bpath=Just p- , bpos=prPos }) ->- L.elem pos0 $ shiftPath prPos p- _ -> False))- ->- let unknownId = ouniqGroup "unknown space"- in ('*', case (vis, F.Walkable `elem` tfeature tk) of- _ | tile == unknownId -> Color.BrBlack- (True, True) -> Color.BrGreen- (True, False) -> Color.BrRed- (False, True) -> Color.Green- (False, False) -> Color.Red)- (Just (aid, m), _) -> viewActor aid m+ vcolor+ | smarkSuspect && Tile.isSuspect cotile tile = Color.BrCyan+ | vis = tcolor tk+ | otherwise = tcolor2 tk+ viewItem i =+ ( jsymbol i+ , Color.defAttr {Color.fg = flavourToColor $ jflavour i} )+ fgOnPathOrLine = case (vis, Tile.isWalkable cotile tile) of+ _ | tile == unknownId -> Color.BrBlack+ _ | Tile.isSuspect cotile tile -> Color.BrCyan+ (True, True) -> Color.BrGreen+ (True, False) -> Color.BrRed+ (False, True) -> Color.Green+ (False, False) -> Color.Red+ atttrOnPathOrLine = if Just pos0 `elem` [cursorPos, tgtPos]+ then inverseVideo {Color.fg = fgOnPathOrLine}+ else Color.defAttr {Color.fg = fgOnPathOrLine}+ (char, attr0) =+ case find (\(_, m) -> pos0 == Actor.bpos m) actorsHere of+ _ | isJust stgtMode+ && (elem pos0 bl || elem pos0 shiftedBTrajectory) ->+ ('*', atttrOnPathOrLine) -- line takes precedence over path+ _ | isJust stgtMode+ && (maybe False (elem pos0) mpath) ->+ (';', atttrOnPathOrLine)+ Just (aid, m) -> viewActor aid m _ | smarkSmell && smlt > timeZero -> (timeToDigit smellTimeout smlt, rainbow pos0) | otherwise -> case EM.keys items of- [] -> (tsymbol tk, vcolor tk)- i : _ -> Item.viewItem $ getItemBody i s+ [] -> (tsymbol tk, Color.defAttr {Color.fg = vcolor})+ i : _ -> viewItem $ getItemBody i s vis = ES.member pos0 $ totalVisible per- visPl =- maybe False (\leader -> actorSeesPos per leader pos0) mleader- bg0 = if isJust stgtMode && Just pos0 == scursor- then Color.defFG -- highlight target cursor- else sVisBG vis visPl -- FOV debug or standard bg- reverseVideo = Color.Attr{ fg = Color.bg Color.defAttr- , bg = Color.fg Color.defAttr- }- optVisually attr@Color.Attr{fg, bg} =- if (fg == Color.defBG)- || (bg == Color.defFG && fg == Color.defFG)- then reverseVideo- else attr+ visPl = case (mleader, bfsmpathRaw) of+ (Just leader, Just (bfs, _)) ->+ let Actor{bpos} = getActorBody leader s+ in posAimsPos bfs bpos pos0+ _ -> False a = case dm of ColorBW -> Color.defAttr- ColorFull -> optVisually Color.Attr{fg = fg0, bg = bg0}- in case over pxy of- Just c -> Color.AttrChar Color.defAttr c- _ -> Color.AttrChar a char- leaderStatus = drawLeaderStatus cops s sdisco ltime mleader- seenN = 100 * lseen `div` lclear- seenTxt | seenN == 100 = "all"- | otherwise = T.justifyRight 2 ' ' (showT seenN) <> "%"- lvlN = T.justifyLeft 2 ' ' (showT $ abs ldepth)- stats =- T.justifyLeft 11 ' ' ("[" <> seenTxt <+> "seen]") <+>- T.justifyLeft 9 ' ' ("$:" <+> showT wealth) <+>- leaderStatus- 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.+ ColorFull ->+ if smarkVision+ then if visPl+ then attr0 {Color.bg = Color.Magenta}+ else if vis+ then attr0 {Color.bg = Color.Blue}+ else attr0+ else attr0+ in Color.AttrChar a char+ showN2 n = T.justifyRight 2 ' ' (tshow n)+ addAttr t = map (Color.AttrChar Color.defAttr) (T.unpack t)+ arenaStatus = drawArenaStatus (ES.member drawnLevelId sexplored) lvl+ cursorText = (if isJust stgtMode then "cursor>" else "Cursor:")+ <+> cursorDesc+ lineText = let space = 40 - T.length cursorText - 1+ lText | blLength == 0 = ""+ | otherwise = "(line" <+> showN2 blLength <> ")"+ in if T.length lText > space+ then ""+ else T.justifyRight space ' ' lText+ cursorStatus = addAttr $ T.justifyLeft 40 ' ' $ cursorText <+> lineText+ selectedStatus = drawSelected cli s drawnLevelId mleader+ leaderStatus = drawLeaderStatus cops s sdisco swaitTimes mleader+ targetText = "Target:" <+> targetDesc+ pathText = let space = 40 - T.length targetText - 1+ len = case (tgtPos, bfsmpathRaw) of+ (Just target, Just (bfs, _)) ->+ fromMaybe 0 (accessBfs bfs target)+ _ -> 0+ pText | len == 0 = ""+ | otherwise = "(path" <+> showN2 len <> ")"+ in if T.length pText > space+ then ""+ else T.justifyRight space ' ' pText+ targetStatus = addAttr $ T.justifyLeft 40 ' ' $ targetText <+> pathText+ sfBottom =+ [ encodeLine $ arenaStatus ++ cursorStatus+ , encodeLine $ selectedStatus ++ leaderStatus ++ targetStatus ]+ fLine y = encodeLine $+ let f l x = let ac = dis $ Point x y in ac : l+ in 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 - 1) $ fromMaybe status msgBottom+ in foldl' f [] [lysize-1,lysize-2..0] in SingleFrame{..} -drawLeaderStatus :: Kind.COps -> State -> Discovery -> Time -> Maybe ActorId- -> Text-drawLeaderStatus cops s sdisco ltime mleader =- case mleader of- Just leader ->- let Kind.COps{coactor=Kind.Ops{okind}} = cops- (bitems, bracedL, ahpS, bhpS) =- let mpl@Actor{bkind, bhp} = getActorBody leader s- ActorKind{ahp} = okind bkind- in (getActorItem leader s, braced mpl ltime,- showT (maxDice ahp), showT bhp)- damage = case Item.strongestSword cops bitems of- Just (_, (_, sw)) ->- case Item.jkind sdisco sw of- Just _ ->- case jeffect sw of- Hurt dice p -> showT dice <> "+" <> showT p- _ -> ""- Nothing -> "3d1" -- TODO: ?- Nothing -> "3d1" -- TODO; use the item 'fist'- -- Indicate the actor is braced (was waiting last move).- -- It's a useful feedback for the otherwise hard to observe- -- 'wait' command.- braceSign | bracedL = "{"- | otherwise = " "- in T.justifyLeft 11 ' ' ("Dmg:" <+> damage) <+>- T.justifyLeft 13 ' ' (braceSign <> "HP:" <+> bhpS- <+> "(" <> ahpS <> ")")- Nothing ->- T.justifyLeft 11 ' ' ("Dmg:" <+> "---") <+>- T.justifyLeft 13 ' ' (" " <> "HP:" <+> "--"- <+> "(" <> "--" <> ")")+inverseVideo :: Color.Attr+inverseVideo = Color.Attr { Color.fg = Color.bg Color.defAttr+ , Color.bg = Color.fg Color.defAttr }++drawArenaStatus :: Bool -> Level -> [Color.AttrChar]+drawArenaStatus explored Level{ldepth, ldesc, lseen, lclear} =+ let addAttr t = map (Color.AttrChar Color.defAttr) (T.unpack t)+ seenN = 100 * lseen `div` lclear+ seenTxt | explored || seenN >= 100 = "all"+ | otherwise = T.justifyLeft 3 ' ' (tshow seenN <> "%")+ lvlN = T.justifyLeft 2 ' ' (tshow $ abs ldepth)+ seenStatus = T.justifyLeft 11 ' ' ("[" <> seenTxt <+> "seen]")+ in addAttr $ lvlN <+> T.justifyLeft 25 ' ' ldesc <+> seenStatus++drawLeaderStatus :: Kind.COps -> State -> Discovery+ -> Int -> Maybe ActorId+ -> [Color.AttrChar]+drawLeaderStatus cops s sdisco waitTimes mleader =+ let addAttr t = map (Color.AttrChar Color.defAttr) (T.unpack t)+ stats = case mleader of+ Just leader ->+ let Kind.COps{coactor=Kind.Ops{okind}} = cops+ (bitems, bracedL, ahpS, bhpS) =+ let mpl@Actor{bkind, bhp} = getActorBody leader s+ ActorKind{ahp} = okind bkind+ in (getActorItem leader s, braced mpl,+ tshow (maxDice ahp), tshow bhp)+ damage = case Item.strongestSword cops bitems of+ Just (_, (_, sw)) ->+ case Item.jkind sdisco sw of+ Just _ ->+ case jeffect sw of+ Hurt dice p -> tshow dice <> "+" <> tshow p+ _ -> ""+ Nothing -> "5d1" -- TODO: ?+ Nothing -> "5d1" -- TODO; use the item 'fist'+ -- Indicate the actor is braced (was waiting last move).+ -- It's a useful feedback for the otherwise hard to observe+ -- 'wait' command.+ slashes = ["|", "\\", "|", "/"]+ slashPick | bracedL =+ slashes !! (max 0 (waitTimes - 1) `mod` length slashes)+ | otherwise = "/"+ bracePick | bracedL = "}"+ | otherwise = ":"+ hpText = bhpS <> slashPick <> ahpS+ in T.justifyLeft 12 ' '+ ("Dmg:" <> (if T.length damage < 8 then " " else "") <> damage)+ <+> "HP" <> bracePick <> T.justifyRight 7 ' ' hpText+ <> " "+ Nothing ->+ T.justifyLeft 12 ' ' "Dmg: ---"+ <+> T.justifyRight 10 ' ' "HP: --/--"+ <> " "+ in addAttr stats++drawSelected :: StateClient -> State -> LevelId -> Maybe ActorId+ -> [Color.AttrChar]+drawSelected cli s drawnLevelId mleader =+ let selected = sselected cli+ viewOurs (aid, Actor{bsymbol, bcolor, bhp})+ | otherwise =+ let cattr = Color.defAttr {Color.fg = bcolor}+ sattr+ | Just aid == mleader = inverseVideo+ | ES.member aid selected =+ -- TODO: in the future use a red rectangle instead+ -- of background and mark them on the map, too;+ -- also, perhaps blink all selected on the map,+ -- when selection changes+ if bcolor /= Color.Blue+ then cattr {Color.bg = Color.Blue}+ else cattr {Color.bg = Color.Magenta}+ | otherwise = cattr+ in ( (bhp > 0, bsymbol /= '@', bsymbol, bcolor, aid)+ , Color.AttrChar sattr bsymbol )+ ours = actorNotProjAssocs (== sside cli) drawnLevelId s+ maxViewed = 14+ -- Don't show anything if the only actor on the level is the leader.+ -- He's clearly highlighted on the level map, anyway.+ star = let sattr = case ES.size selected of+ 0 -> Color.defAttr {Color.fg = Color.BrBlack}+ n | n == length ours ->+ Color.defAttr {Color.bg = Color.Blue}+ _ -> Color.defAttr+ char = if length ours > maxViewed then '$' else '*'+ in Color.AttrChar sattr char+ viewed = take maxViewed $ sort $ map viewOurs ours+ addAttr t = map (Color.AttrChar Color.defAttr) (T.unpack t)+ allOurs = filter ((== sside cli) . bfid) $ EM.elems $ sactorD s+ nameN n t = T.justifyLeft n ' '+ $ let firstWord = head $ T.words t+ in if T.length firstWord > n then "" else firstWord+ fact = sfactionD s EM.! sside cli+ ourName n = addAttr $ nameN n $ playerName $ gplayer fact+ party = if length allOurs <= 1+ then ourName $ maxViewed + 1+ else [star] ++ map snd viewed ++ addAttr " "+ ++ ourName (maxViewed - 1 - length viewed)+ in party ++ addAttr (T.replicate (maxViewed + 2 - length party) " ")
− Game/LambdaHack/Client/HumanCmd.hs
@@ -1,152 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}--- | Abstract syntax human player commands.-module Game.LambdaHack.Client.HumanCmd- ( HumanCmd(..), Trigger(..)- , majorHumanCmd, minorHumanCmd, noRemoteHumanCmd, cmdDescription- ) where--import Data.Binary-import Data.Text (Text)-import GHC.Generics (Generic)-import qualified NLP.Miniutter.English as MU--import Control.Exception.Assert.Sugar-import qualified Game.LambdaHack.Common.Feature as F-import Game.LambdaHack.Common.Msg-import Game.LambdaHack.Common.VectorXY---- | Abstract syntax of player commands.-data HumanCmd =- -- These usually take time.- Move !VectorXY- | Run !VectorXY- | Wait- | Pickup- | Drop- | Project ![Trigger]- | Apply ![Trigger]- | AlterDir ![Trigger]- | TriggerTile ![Trigger]- -- These do not take time.- | GameRestart !Text- | GameExit- | GameSave- -- These do not notify the server.- | SelectHero !Int- | MemberCycle- | MemberBack- | Inventory- | TgtFloor- | TgtEnemy- | TgtAscend !Int- | EpsIncr !Bool- | Cancel- | Accept- | Clear- | History- | MarkVision- | MarkSmell- | MarkSuspect- | Help- deriving (Show, Read, Eq, Ord, Generic)--instance Binary HumanCmd--data Trigger =- ApplyItem {verb :: !MU.Part, object :: !MU.Part, symbol :: !Char}- | AlterFeature {verb :: !MU.Part, object :: !MU.Part, feature :: !F.Feature}- | TriggerFeature {verb :: !MU.Part, object :: !MU.Part, feature :: !F.Feature}- deriving (Show, Read, Eq, Ord, Generic)--instance Binary Trigger---- | Major commands land on the first page of command help.-majorHumanCmd :: HumanCmd -> Bool-majorHumanCmd cmd = case cmd of- Pickup -> True- Drop -> True- Project{} -> True- Apply{} -> True- AlterDir{} -> True- TriggerTile{} -> True- Inventory -> True- Help -> True- _ -> False---- | Minor commands land on the second page of command help.-minorHumanCmd :: HumanCmd -> Bool-minorHumanCmd cmd = case cmd of- MemberCycle -> True- MemberBack -> True- TgtFloor -> True- TgtEnemy -> True- TgtAscend{} -> True- EpsIncr{} -> True- Cancel -> True- Accept -> True--- Clear -> True- History -> True- MarkVision -> True- MarkSmell -> True- MarkSuspect -> True- _ -> False---- | Commands that are forbidden on a remote level, because they--- would usually take time when invoked on one.--- Not that movement commands are not included, because they take time--- on normal levels, but don't take time on remote levels, that is,--- in targeting mode.-noRemoteHumanCmd :: HumanCmd -> Bool-noRemoteHumanCmd cmd = case cmd of- Wait -> True- Pickup -> True- Drop -> True- Project{} -> True- Apply{} -> True- AlterDir{} -> True- TriggerTile{} -> True- _ -> False---- | Description of player commands.-cmdDescription :: HumanCmd -> Text-cmdDescription cmd = case cmd of- Move{} -> "move"- Run{} -> "run"- Wait -> "wait"- Pickup -> "get an object"- Drop -> "drop an object"- Project ts -> triggerDescription ts- Apply ts -> triggerDescription ts- AlterDir ts -> triggerDescription ts- TriggerTile ts -> triggerDescription ts-- GameRestart t -> "new" <+> t <+> "game"- GameExit -> "save and exit"- GameSave -> "save game"-- SelectHero{} -> "select hero"- MemberCycle -> "cycle among heroes on the level"- MemberBack -> "cycle among heroes in the dungeon"- Inventory -> "display inventory"- TgtFloor -> "target position"- TgtEnemy -> "target monster"- TgtAscend k | k == 1 -> "target next shallower level"- TgtAscend k | k >= 2 -> "target" <+> showT k <+> "levels shallower"- TgtAscend k | k == -1 -> "target next deeper level"- TgtAscend k | k <= -2 -> "target" <+> showT (-k) <+> "levels deeper"- TgtAscend _ ->- assert `failure` "void level change when targeting" `twith` cmd- EpsIncr True -> "swerve targeting line"- EpsIncr False -> "unswerve targeting line"- Cancel -> "cancel action"- Accept -> "accept choice"- Clear -> "clear messages"- History -> "display previous messages"- MarkVision -> "mark visible area"- MarkSmell -> "mark smell"- MarkSuspect -> "mark suspect terrain"- Help -> "display help"--triggerDescription :: [Trigger] -> Text-triggerDescription [] = "trigger a thing"-triggerDescription (t : _) = makePhrase [verb t, MU.AW $ object t]
Game/LambdaHack/Client/HumanGlobal.hs view
@@ -2,32 +2,42 @@ -- A couple of them do not take time, the rest does. -- TODO: document module Game.LambdaHack.Client.HumanGlobal- ( moveRunAid, displaceAid, meleeAid, waitHuman, pickupHuman, dropHuman- , projectAid, applyHuman, alterDirHuman, triggerTileHuman+ ( -- * Commands that usually take time+ moveRunHuman, waitHuman, pickupHuman, dropHuman+ , projectHuman, applyHuman, alterDirHuman, triggerTileHuman+ , stepToTargetHuman, resendHuman+ -- * Commands that never take time , gameRestartHuman, gameExitHuman, gameSaveHuman+ -- * Helper definitions+ , SlideOrCmd, failWith ) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES import Data.Function import Data.List import Data.Maybe+import Data.Monoid import Data.Text (Text) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU -import Control.Exception.Assert.Sugar import Game.LambdaHack.Client.Action+import Game.LambdaHack.Client.Config import Game.LambdaHack.Client.Draw-import Game.LambdaHack.Client.HumanCmd (Trigger (..)) import Game.LambdaHack.Client.HumanLocal+import Game.LambdaHack.Client.RunAction import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.Animation import qualified Game.LambdaHack.Common.Effect as Effect import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.HumanCmd (Trigger (..)) import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Key as K import qualified Game.LambdaHack.Common.Kind as Kind@@ -39,154 +49,212 @@ import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.TileKind as TileKind -abortFailure :: MonadClientAbort m => FailureSer -> m a-abortFailure = abortWith . showFailureSer+type SlideOrCmd a = Either Slideshow a +failWith :: MonadClientUI m => Msg -> m (SlideOrCmd a)+failWith msg = do+ modifyClient $ \cli -> cli {slastKey = Nothing}+ stopPlayBack+ assert (not $ T.null msg) $ fmap Left $ promptToSlideshow msg++failSlides :: MonadClientUI m => Slideshow -> m (SlideOrCmd a)+failSlides slides = do+ modifyClient $ \cli -> cli {slastKey = Nothing}+ stopPlayBack+ return $ Left slides++failSer :: MonadClientUI m => FailureSer -> m (SlideOrCmd a)+failSer = failWith . showFailureSer+ -- * Move and Run +moveRunHuman :: MonadClientUI m+ => Bool -> Vector -> m (SlideOrCmd CmdTakeTimeSer)+moveRunHuman run dir = do+ tgtMode <- getsClient stgtMode+ if isJust tgtMode then+ fmap Left $ moveCursorHuman dir (if run then 10 else 1)+ else do+ arena <- getArenaUI+ leader <- getLeaderUI+ sb <- getsState $ getActorBody leader+ fact <- getsState $ (EM.! bfid sb) . sfactionD+ let tpos = bpos sb `shift` dir+ -- We start by checking actors at the the target position,+ -- which gives a partial information (actors can be invisible),+ -- as opposed to accessibility (and items) which are always accurate+ -- (tiles can't be invisible).+ tgts <- getsState $ posToActors tpos arena+ case tgts of+ [] -> do -- move or search or alter+ -- Start running in the given direction. The first turn of running+ -- succeeds much more often than subsequent turns, because we ignore+ -- most of the disturbances, since the player is mostly aware of them+ -- and still explicitly requests a run, knowing how it behaves.+ runStopOrCmd <- moveRunAid leader dir+ case runStopOrCmd of+ Left stopMsg -> failWith stopMsg+ Right runCmd -> do+ sel <- getsClient sselected+ let runMembers = if isSpawnFact fact+ then [leader] -- TODO: warn?+ else ES.toList (ES.delete leader sel) ++ [leader]+ runParams = RunParams { runLeader = leader+ , runMembers+ , runDist = 0+ , runStopMsg = Nothing+ , runInitDir = Just dir }+ when run $ modifyClient $ \cli -> cli {srunning = Just runParams}+ return $ Right runCmd+ -- When running, the invisible actor is hit (not displaced!),+ -- so that running in the presence of roving invisible+ -- actors is equivalent to moving (with visible actors+ -- this is not a problem, since runnning stops early enough).+ -- TODO: stop running at invisible actor+ [((target, _), _)] | run ->+ -- Displacing requires accessibility, but it's checked later on.+ displaceAid leader target+ _ : _ : _ | run -> do+ assert (all (bproj . snd . fst) tgts) skip+ failSer DisplaceProjectiles+ ((target, tb), _) : _ -> do+ -- No problem if there are many projectiles at the spot. We just+ -- attack the first one.+ -- We always see actors from our own faction.+ if bfid tb == bfid sb && not (bproj tb) then do+ if isSpawnFact fact then+ failWith "spawners cannot manually change leaders"+ else do+ -- Select adjacent actor by bumping into him. Takes no time.+ success <- pickLeader target+ assert (success `blame` "bump self"+ `twith` (leader, target, tb)) skip+ return $ Left mempty+ else+ -- Attacking does not require full access, adjacency is enough.+ meleeAid leader target+ -- | Actor atttacks an enemy actor or his own projectile.-meleeAid :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> ActorId -> m CmdSerTakeTime+meleeAid :: MonadClientUI m+ => ActorId -> ActorId -> m (SlideOrCmd CmdTakeTimeSer) meleeAid source target = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target sfact <- getsState $ (EM.! bfid sb) . sfactionD- unless (bproj tb || isAtWar sfact (bfid tb)) $ do- go <- displayYesNo ColorBW- "This attack will start a war. Are you sure?"- unless go $ abortWith "Attack canceled."- unless (bproj tb || not (isAllied sfact (bfid tb))) $ do- go <- displayYesNo ColorBW- "You are bound by an alliance. Really attack?"- unless go $ abortWith "Attack canceled."- return $ MeleeSer source target+ let returnCmd = return $ Right $ MeleeSer source target+ if not (bproj tb || isAtWar sfact (bfid tb)) then do+ go1 <- displayYesNo ColorBW+ "This attack will start a war. Are you sure?"+ if not go1 then failWith "Attack canceled."+ else if not (bproj tb || not (isAllied sfact (bfid tb))) then do+ go2 <- displayYesNo ColorBW+ "You are bound by an alliance. Really attack?"+ if not go2 then failWith "Attack canceled."+ else returnCmd+ else returnCmd+ else returnCmd -- Seeing the actor prevents altering a tile under it, but that -- does not limit the player, he just doesn't waste a turn -- on a failed altering. -- | Actor swaps position with another.-displaceAid :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> ActorId -> m CmdSerTakeTime+displaceAid :: MonadClientUI m+ => ActorId -> ActorId -> m (SlideOrCmd CmdTakeTimeSer) displaceAid source target = do cops <- getsState scops sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target- let lid = blid sb- lvl <- getLevel lid- let spos = bpos sb- tpos = bpos tb- if accessible cops lvl spos tpos then+ let adj = checkAdjacent sb tb+ if not adj then failSer DisplaceDistant+ else do+ let lid = blid sb+ lvl <- getLevel lid+ let spos = bpos sb+ tpos = bpos tb -- Displacing requires full access.- return $ DisplaceSer source target- else abortFailure DisplaceAccess---- | Actor moves or searches or alters. No visible actor at the position.-moveRunAid :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> Vector -> m CmdSerTakeTime-moveRunAid source dir = do- cops@Kind.COps{cotile} <- getsState scops- sb <- getsState $ getActorBody source- let lid = blid sb- lvl <- getLevel lid- let spos = bpos sb -- source position- tpos = spos `shift` dir -- target position- t = lvl `at` tpos- -- Movement requires full access.- if accessible cops lvl spos tpos then- -- The potential invisible actor is hit. War is started without asking.- return $ MoveSer source dir- -- No access, so search and/or alter the tile.- else if not (Tile.hasFeature cotile F.Walkable t) -- not implied by access- && (Tile.hasFeature cotile F.Suspect t- || Tile.openable cotile t- || Tile.closable cotile t- || Tile.changeable cotile t) then- if not $ EM.null $ lvl `atI` tpos then abortFailure AlterBlockItem- else return $ AlterSer source tpos Nothing- -- We don't use MoveSer, because we don't hit invisible actors here.- -- The potential invisible actor, e.g., in a wall or in- -- an inaccessible doorway, is made known, taking a turn.- -- If server performed an attack for free on the invisible actor anyway,- -- the player (or AI) would be tempted to repeatedly- -- hit random walls in hopes of killing a monster lurking within.- -- If the action had a cost, misclicks would incur the cost, too.- -- Right now the player may repeatedly alter tiles trying to learn- -- about invisible pass-wall actors, but it costs a turn- -- and does not harm the invisible actors, so it's not tempting.- else- -- Ignore a known boring, not accessible tile.- neverMind True+ if accessible cops lvl spos tpos then do+ tgts <- getsState $ posToActors tpos lid+ case tgts of+ [] -> assert `failure` (source, sb, target, tb)+ [_] -> do+ return $ Right $ DisplaceSer source target+ _ -> failSer DisplaceProjectiles+ else failSer DisplaceAccess -- * Wait -- | Leader waits a turn (and blocks, etc.).-waitHuman :: MonadClientUI m => m CmdSerTakeTime+waitHuman :: MonadClientUI m => m CmdTakeTimeSer waitHuman = do+ modifyClient $ \cli -> cli {swaitTimes = abs (swaitTimes cli) + 1} leader <- getLeaderUI- return $ WaitSer leader+ return $! WaitSer leader -- * Pickup -pickupHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSerTakeTime+pickupHuman :: MonadClientUI m => m (SlideOrCmd CmdTakeTimeSer) pickupHuman = do leader <- getLeaderUI body <- getsState $ getActorBody leader lvl <- getLevel $ blid body -- Check if something is here to pick up. Items are never invisible. case EM.minViewWithKey $ lvl `atI` bpos body of- Nothing -> abortWith "nothing here"+ Nothing -> failWith "nothing here" Just ((iid, k), _) -> do -- pick up first item; TODO: let pl select item item <- getsState $ getItemBody iid let l = if jsymbol item == '$' then Just $ InvChar '$' else Nothing case assignLetter iid l body of- Just l2 -> return $ PickupSer leader iid k l2- Nothing -> abortWith "cannot carry any more"+ Just _ -> return $ Right $ PickupSer leader iid k+ Nothing -> failSer PickupOverfull -- * Drop --- TODO: you can drop an item already on the floor, which works correctly,--- but is weird and useless.+-- TODO: you can drop an item already on the floor (the '-' is there),+-- which is weird and useless. -- | Drop a single item.-dropHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSerTakeTime+dropHuman :: MonadClientUI m => m (SlideOrCmd CmdTakeTimeSer) dropHuman = do -- TODO: allow dropping a given number of identical items. Kind.COps{coitem} <- getsState scops leader <- getLeaderUI bag <- getsState $ getActorBag leader inv <- getsState $ getActorInv leader- ((iid, item), (_, container)) <-- getAnyItem leader "What to drop?" bag inv "in inventory"- case container of- CFloor{} -> neverMind True- CActor aid _ -> do- assert (aid == leader) skip- disco <- getsClient sdisco- subject <- partAidLeader leader- msgAdd $ makeSentence- [ MU.SubjectVerbSg subject "drop"- , partItemWs coitem disco 1 item ]- return $ DropSer leader iid+ ggi <- getAnyItem leader "What to drop?" bag inv "in inventory"+ case ggi of+ Right ((iid, item), (_, container)) ->+ case container of+ CFloor{} -> failWith "never mind"+ CActor aid _ -> do+ assert (aid == leader) skip+ disco <- getsClient sdisco+ subject <- partAidLeader leader+ msgAdd $ makeSentence+ [ MU.SubjectVerbSg subject "drop"+ , partItemWs coitem disco 1 item ]+ return $ Right $ DropSer leader iid 1+ Left slides -> return $ Left slides allObjectsName :: Text allObjectsName = "Objects" -- | Let the human player choose any item from a list of items.-getAnyItem :: (MonadClientAbort m, MonadClientUI m)+getAnyItem :: MonadClientUI m => ActorId -> Text -- ^ prompt -> ItemBag -- ^ all items in question -> ItemInv -- ^ inventory characters -> Text -- ^ how to refer to the collection of items- -> m ((ItemId, Item), (Int, Container))+ -> m (SlideOrCmd ((ItemId, Item), (Int, Container))) getAnyItem leader prompt = getItem leader prompt (const True) allObjectsName data ItemDialogState = INone | ISuitable | IAll deriving Eq -- | Let the human player choose a single, preferably suitable, -- item from a list of items.-getItem :: (MonadClientAbort m, MonadClientUI m)+getItem :: MonadClientUI m => ActorId -> Text -- ^ prompt message -> (Item -> Bool) -- ^ which items to consider suitable@@ -194,16 +262,16 @@ -> ItemBag -- ^ all items in question -> ItemInv -- ^ inventory characters -> Text -- ^ how to refer to the collection of items- -> m ((ItemId, Item), (Int, Container))-getItem aid prompt p ptext bag inv isn = do+ -> m (SlideOrCmd ((ItemId, Item), (Int, Container)))+getItem aid prompt p ptext bag invRaw isn = do leader <- getLeaderUI b <- getsState $ getActorBody leader lvl <- getLevel $ blid b s <- getState body <- getsState $ getActorBody aid- let checkItem (l, iid) =- fmap (\k -> ((iid, getItemBody iid s), (k, l))) $ EM.lookup iid bag- is0 = mapMaybe checkItem $ EM.assocs inv+ let inv = EM.filter (`EM.member` bag) invRaw+ checkItem (l, iid) = ((iid, getItemBody iid s), (bag EM.! iid, l))+ is0 = map checkItem $ EM.assocs inv pos = bpos body tis = lvl `atI` pos floorFull = not $ EM.null tis@@ -228,9 +296,9 @@ r = letterRange mls in "[" <> r <> ", ?" <> floorMsg <> bestMsg ask = do- when (null is0 && EM.null tis) $- abortWith "Not carrying anything."- perform INone+ if null is0 && EM.null tis+ then failWith "Not carrying anything."+ else perform INone invP = EM.filter (\iid -> p (getItemBody iid s)) inv perform itemDialogState = do let (ims, invOver, msg) = case itemDialogState of@@ -238,43 +306,59 @@ ISuitable -> (isp, invP, ptext <+> isn <> ".") IAll -> (is0, inv, allObjectsName <+> isn <> ".") io <- itemOverlay bag invOver- km@K.KM {..} <-- displayChoiceUI (msg <+> choice ims) io (keys ims)- assert (modifier == K.NoModifier) skip- case key of- K.Char '?' -> case itemDialogState of- INone -> perform ISuitable- ISuitable | ptext /= allObjectsName -> perform IAll- _ -> perform INone- K.Char '-' | floorFull ->- -- TODO: let player select item- return $ maximumBy (compare `on` fst . fst)- $ map (\(iid, k) ->- ((iid, getItemBody iid s),- (k, CFloor (blid b) pos)))- $ EM.assocs tis- K.Char l ->- case find ((InvChar l ==) . snd . snd) ims of- Nothing -> assert `failure` "unexpected inventory letter"- `twith` (km, l, ims)- Just (iidItem, (k, l2)) ->- return (iidItem, (k, CActor aid l2))- K.Return | bestFull ->- let (iidItem, (k, l2)) = maximumBy (compare `on` snd . snd) isp- in return (iidItem, (k, CActor aid l2))- _ -> assert `failure` "unexpected key:" `twith` km+ akm <- displayChoiceUI (msg <+> choice ims) io (keys is0)+ case akm of+ Left slides -> failSlides slides+ Right (km@K.KM {..}) -> do+ assert (modifier == K.NoModifier) skip+ case key of+ K.Char '?' -> case itemDialogState of+ INone -> if EM.null invP+ then perform IAll+ else perform ISuitable+ ISuitable | ptext /= allObjectsName -> perform IAll+ _ -> perform INone+ K.Char '-' | floorFull ->+ -- TODO: let player select item+ return $ Right+ $ maximumBy (compare `on` fst . fst)+ $ map (\(iid, k) ->+ ((iid, getItemBody iid s),+ (k, CFloor (blid b) pos)))+ $ EM.assocs tis+ K.Char l ->+ case find ((InvChar l ==) . snd . snd) is0 of+ Nothing -> assert `failure` "unexpected inventory letter"+ `twith` (km, l, is0)+ Just (iidItem, (k, l2)) ->+ return $ Right (iidItem, (k, CActor aid l2))+ K.Return | bestFull ->+ let (iidItem, (k, l2)) = maximumBy (compare `on` snd . snd) isp+ in return $ Right (iidItem, (k, CActor aid l2))+ _ -> assert `failure` "unexpected key:" `twith` km ask -- * Project -projectAid :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> [Trigger] -> m CmdSerTakeTime-projectAid source ts = do- Kind.COps{cotile} <- getsState scops- target <- targetToPos- let tpos = case target of- Just p -> p- Nothing -> assert `failure` "target unexpectedly invalid" `twith` source+projectHuman :: MonadClientUI m+ => [Trigger] -> m (SlideOrCmd CmdTakeTimeSer)+projectHuman ts = do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ tgtPos <- leaderTgtToPos+ case tgtPos of+ Nothing -> failWith "last target invalid"+ Just pos | pos == bpos b -> failWith "cannot aim at oneself"+ Just pos -> do+ canAim <- leaderTgtAims+ case canAim of+ Nothing -> projectAid leader ts pos+ Just cause -> failWith cause++projectAid :: MonadClientUI m+ => ActorId -> [Trigger] -> Point -> m (SlideOrCmd CmdTakeTimeSer)+projectAid source ts tpos = do+ Kind.COps{coactor=Kind.Ops{okind}, cotile} <- getsState scops eps <- getsClient seps sb <- getsState $ getActorBody source let lid = blid sb@@ -283,24 +367,28 @@ Level{lxsize, lysize} <- getLevel lid foes <- getsState $ actorNotProjList (isAtWar fact) lid if foesAdjacent lxsize lysize spos foes- then abortFailure ProjectBlockFoes+ then failSer ProjectBlockFoes else do case bla lxsize lysize eps spos tpos of- Nothing -> abortFailure ProjectAimOnself+ Nothing -> failSer ProjectAimOnself Just [] -> assert `failure` "project from the edge of level" `twith` (spos, tpos, sb, ts) Just (pos : _) -> do- as <- getsState $ actorList (const True) lid lvl <- getLevel lid let t = lvl `at` pos- if not $ Tile.hasFeature cotile F.Clear t- then abortFailure ProjectBlockTerrain- else if unoccupied as pos- then projectBla source tpos eps ts- else abortFailure ProjectBlockActor+ if not $ Tile.isWalkable cotile t+ then failSer ProjectBlockTerrain+ else do+ mab <- getsState $ posToActor pos lid+ if maybe True (bproj . snd . fst) mab+ then if not (asight $ okind $ bkind sb)+ then failSer ProjectBlind+ else projectBla source tpos eps ts+ else failSer ProjectBlockActor -projectBla :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> Point -> Int -> [Trigger] -> m CmdSerTakeTime+projectBla :: MonadClientUI m+ => ActorId -> Point -> Int -> [Trigger]+ -> m (SlideOrCmd CmdTakeTimeSer) projectBla source tpos eps ts = do let (verb1, object1) = case ts of [] -> ("aim", "object")@@ -308,14 +396,12 @@ triggerSyms = triggerSymbols ts bag <- getsState $ getActorBag source inv <- getsState $ getActorInv source- ((iid, _), (_, container)) <-- getGroupItem source bag inv object1 triggerSyms- (makePhrase ["What to", verb1 MU.:> "?"]) "in inventory"- stgtMode <- getsClient stgtMode- case stgtMode of- Just (TgtAuto _) -> endTargeting True- _ -> return ()- return $! ProjectSer source tpos eps iid container+ ggi <- getGroupItem source bag inv object1 triggerSyms+ (makePhrase ["What to", verb1 MU.:> "?"]) "in inventory"+ case ggi of+ Right ((iid, _), (_, container)) ->+ return $ Right $ ProjectSer source tpos eps iid container+ Left slides -> return $ Left slides triggerSymbols :: [Trigger] -> [Char] triggerSymbols [] = []@@ -324,8 +410,7 @@ -- * Apply -applyHuman :: (MonadClientAbort m, MonadClientUI m)- => [Trigger] -> m CmdSerTakeTime+applyHuman :: MonadClientUI m => [Trigger] -> m (SlideOrCmd CmdTakeTimeSer) applyHuman ts = do leader <- getLeaderUI bag <- getsState $ getActorBag leader@@ -334,15 +419,17 @@ [] -> ("activate", "object") tr : _ -> (verb tr, object tr) triggerSyms = triggerSymbols ts- ((iid, _), (_, container)) <-- getGroupItem leader bag inv object1 triggerSyms- (makePhrase ["What to", verb1 MU.:> "?"]) "in inventory"- return $! ApplySer leader iid container+ ggi <- getGroupItem leader bag inv object1 triggerSyms+ (makePhrase ["What to", verb1 MU.:> "?"]) "in inventory"+ case ggi of+ Right ((iid, _), (_, container)) ->+ return $ Right $ ApplySer leader iid container+ Left slides -> return $ Left slides -- | Let a human 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 :: (MonadClientAbort m, MonadClientUI m)+getGroupItem :: MonadClientUI m => ActorId -> ItemBag -- ^ all objects in question -> ItemInv -- ^ inventory characters@@ -350,7 +437,7 @@ -> [Char] -- ^ accepted item symbols -> Text -- ^ prompt -> Text -- ^ how to refer to the collection of objects- -> m ((ItemId, Item), (Int, Container))+ -> m (SlideOrCmd ((ItemId, Item), (Int, Container))) getGroupItem leader is inv object syms prompt packName = do let choice i = jsymbol i `elem` syms header = makePhrase [MU.Capitalize (MU.Ws object)]@@ -359,23 +446,24 @@ -- * AlterDir -- | Ask for a direction and alter a tile, if possible.-alterDirHuman :: (MonadClientAbort m, MonadClientUI m)- => [Trigger] -> m CmdSerTakeTime+alterDirHuman :: MonadClientUI m => [Trigger] -> m (SlideOrCmd CmdTakeTimeSer) alterDirHuman ts = do let verb1 = case ts of [] -> "alter" tr : _ -> verb tr keys = zipWith K.KM (repeat K.NoModifier) K.dirAllMoveKey prompt = makePhrase ["What to", verb1 MU.:> "? [movement key"]- e <- displayChoiceUI prompt [] keys- leader <- getLeaderUI- b <- getsState $ getActorBody leader- Level{lxsize} <- getLevel $ blid b- K.handleDir lxsize e (flip (alterTile leader) ts) (neverMind True)+ me <- displayChoiceUI prompt emptyOverlay keys+ case me of+ Left slides -> failSlides slides+ Right e -> do+ leader <- getLeaderUI+ K.handleDir e (flip (alterTile leader) ts) (failWith "never mind") -- | Player tries to alter a tile using a feature.-alterTile :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> Vector -> [Trigger] -> m CmdSerTakeTime+alterTile :: MonadClientUI m+ => ActorId -> Vector -> [Trigger]+ -> m (SlideOrCmd CmdTakeTimeSer) alterTile source dir ts = do Kind.COps{cotile} <- getsState scops b <- getsState $ getActorBody source@@ -384,8 +472,8 @@ t = lvl `at` tpos alterFeats = alterFeatures ts case filter (\feat -> Tile.hasFeature cotile feat t) alterFeats of- [] -> guessAlter cotile alterFeats t- feat : _ -> return $! AlterSer source tpos $ Just feat+ [] -> failWith $ guessAlter cotile alterFeats t+ feat : _ -> return $ Right $ AlterSer source tpos $ Just feat alterFeatures :: [Trigger] -> [F.Feature] alterFeatures [] = []@@ -393,30 +481,39 @@ alterFeatures (_ : ts) = alterFeatures ts -- | Guess and report why the bump command failed.-guessAlter :: MonadClientAbort m- => Kind.Ops TileKind -> [F.Feature] -> Kind.Id TileKind -> m a-guessAlter cotile (F.OpenTo _ : _) t | Tile.closable cotile t =- abortWith "already open"-guessAlter _ (F.OpenTo _ : _) _ =- abortWith "can't be opened"-guessAlter cotile (F.CloseTo _ : _) t | Tile.openable cotile t =- abortWith "already closed"-guessAlter _ (F.CloseTo _ : _) _ =- abortWith "can't be closed"-guessAlter _ _ _ = neverMind True+guessAlter :: Kind.Ops TileKind -> [F.Feature] -> Kind.Id TileKind -> Msg+guessAlter cotile (F.OpenTo _ : _) t+ | Tile.isClosable cotile t = "already open"+guessAlter _ (F.OpenTo _ : _) _ = "cannot be opened"+guessAlter cotile (F.CloseTo _ : _) t+ | Tile.isOpenable cotile t = "already closed"+guessAlter _ (F.CloseTo _ : _) _ = "cannot be closed"+guessAlter _ _ _ = "never mind" -- * TriggerTile -- | Leader tries to trigger the tile he's standing on.-triggerTileHuman :: (MonadClientAbort m, MonadClientUI m)- => [Trigger] -> m CmdSerTakeTime+triggerTileHuman :: MonadClientUI m+ => [Trigger] -> m (SlideOrCmd CmdTakeTimeSer) triggerTileHuman ts = do- leader <- getLeaderUI- triggerTile leader ts+ tgtMode <- getsClient stgtMode+ if isJust tgtMode then do+ let getK tfs = case tfs of+ TriggerFeature {feature = F.Cause (Effect.Ascend k)} : _ -> Just k+ _ : rest -> getK rest+ [] -> Nothing+ mk = getK ts+ case mk of+ Nothing -> failWith "never mind"+ Just k -> fmap Left $ tgtAscendHuman k+ else do+ leader <- getLeaderUI+ triggerTile leader ts -- | Player tries to trigger a tile using a feature.-triggerTile :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> [Trigger] -> m CmdSerTakeTime+triggerTile :: MonadClientUI m+ => ActorId -> [Trigger]+ -> m (SlideOrCmd CmdTakeTimeSer) triggerTile leader ts = do Kind.COps{cotile} <- getsState scops b <- getsState $ getActorBody leader@@ -424,10 +521,12 @@ let t = lvl `at` bpos b triggerFeats = triggerFeatures ts case filter (\feat -> Tile.hasFeature cotile feat t) triggerFeats of- [] -> guessTrigger cotile triggerFeats t+ [] -> failWith $ guessTrigger cotile triggerFeats t feat : _ -> do- verifyTrigger leader feat- return $! TriggerSer leader $ Just feat+ go <- verifyTrigger leader feat+ case go of+ Right () -> return $ Right $ TriggerSer leader $ Just feat+ Left slides -> return $ Left slides triggerFeatures :: [Trigger] -> [F.Feature] triggerFeatures [] = []@@ -435,74 +534,114 @@ triggerFeatures (_ : ts) = triggerFeatures ts -- | Verify important feature triggers, such as fleeing the dungeon.-verifyTrigger :: (MonadClientAbort m, MonadClientUI m)- => ActorId -> F.Feature -> m ()+verifyTrigger :: MonadClientUI m+ => ActorId -> F.Feature -> m (SlideOrCmd ()) verifyTrigger leader feat = case feat of- F.Cause Effect.Escape -> do+ F.Cause Effect.Escape{} -> do+ cops <- getsState scops b <- getsState $ getActorBody leader side <- getsClient sside- spawn <- getsState $ isSpawnFaction side- summon <- getsState $ isSummonFaction side- when (spawn || summon) $ abortWith+ fact <- getsState $ (EM.! side) . sfactionD+ let isHero = isHeroFact cops fact+ if not isHero then failWith "This is the way out, but where would you go in this alien world?"- go <- displayYesNo ColorFull "This is the way out. Really leave now?"- unless go $ abortWith "Game resumed."- (_, total) <- getsState $ calculateTotal b- when (total == 0) $ do- -- The player can back off at each of these steps.- go1 <- displayMore ColorBW- "Afraid of the challenge? Leaving so soon and empty-handed?"- unless go1 $ abortWith "Brave soul!"- go2 <- displayMore ColorBW- "Next time try to grab some loot before escape!"- unless go2 $ abortWith "Here's your chance!"- _ -> return ()+ else do+ go <- displayYesNo ColorFull "This is the way out. Really leave now?"+ if not go then failWith "Game resumed."+ else do+ (_, total) <- getsState $ calculateTotal b+ if total == 0 then do+ -- The player can back off at each of these steps.+ go1 <- displayMore ColorBW+ "Afraid of the challenge? Leaving so soon and empty-handed?"+ if not go1 then failWith "Brave soul!"+ else do+ go2 <- displayMore ColorBW+ "Next time try to grab some loot before escape!"+ if not go2 then failWith "Here's your chance!"+ else return $ Right ()+ else return $ Right ()+ _ -> return $ Right () -- | Guess and report why the bump command failed.-guessTrigger :: MonadClientAbort m- => Kind.Ops TileKind -> [F.Feature] -> Kind.Id TileKind -> m a+guessTrigger :: Kind.Ops TileKind -> [F.Feature] -> Kind.Id TileKind -> Msg guessTrigger cotile fs@(F.Cause (Effect.Ascend k) : _) t | Tile.hasFeature cotile (F.Cause (Effect.Ascend (-k))) t =- if k > 0 then- abortWith "the way goes down, not up"- else if k < 0 then- abortWith "the way goes up, not down"- else- assert `failure` fs+ if k > 0 then "the way goes down, not up"+ else if k < 0 then "the way goes up, not down"+ else assert `failure` fs guessTrigger _ fs@(F.Cause (Effect.Ascend k) : _) _ =- if k > 0 then- abortWith "can't ascend"- else if k < 0 then- abortWith "can't descend"- else- assert `failure` fs-guessTrigger _ _ _ = neverMind True+ if k > 0 then "cannot ascend"+ else if k < 0 then "cannot descend"+ else assert `failure` fs+guessTrigger _ _ _ = "never mind" +-- * StepToTarget++stepToTargetHuman :: MonadClientUI m => m (SlideOrCmd CmdTakeTimeSer)+stepToTargetHuman = do+ tgtMode <- getsClient stgtMode+ -- Movement is legal only outside targeting mode.+ -- TODO: use this command for something in targeting mode.+ if isJust tgtMode then failWith "cannot move in targeting mode"+ else do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ tgtPos <- leaderTgtToPos+ case tgtPos of+ Nothing -> failWith "target not set"+ Just c | c == bpos b -> failWith "target reached"+ Just c -> do+ (_, mpath) <- getCacheBfsAndPath leader c+ case mpath of+ Nothing -> failWith "no route to target"+ Just [] -> assert `failure` (leader, b, bpos b, c)+ Just (p1 : _) -> do+ as <- getsState $ posToActors p1 (blid b)+ if not $ null as then+ failWith "actor in the path to target"+ else+ moveRunHuman False $ towards (bpos b) p1++-- * Resend++resendHuman :: MonadClientUI m => m (SlideOrCmd CmdTakeTimeSer)+resendHuman = do+ slastCmd <- getsClient slastCmd+ case slastCmd of+ Just cmd -> return $ Right cmd+ Nothing -> failWith "no time-taking command to repeat"+ -- * GameRestart; does not take time -gameRestartHuman :: (MonadClientAbort m, MonadClientUI m) => Text -> m CmdSer+gameRestartHuman :: MonadClientUI m => Text -> m (SlideOrCmd CmdSer) gameRestartHuman t = do let msg = "You just requested a new" <+> t <+> "game." b1 <- displayMore ColorFull msg- unless b1 $ neverMind True- b2 <- displayYesNo ColorBW- "Current progress will be lost! Really restart the game?"- msg2 <- rndToAction $ oneOf- [ "Yea, would be a pity to leave them all to die."- , "Yea, a shame to get your own team stranded." ]- unless b2 $ abortWith msg2- leader <- getLeaderUI- return $ GameRestartSer leader t+ if not b1 then failWith "never mind"+ else do+ b2 <- displayYesNo ColorBW+ "Current progress will be lost! Really restart the game?"+ msg2 <- rndToAction $ oneOf+ [ "Yea, would be a pity to leave them all to die."+ , "Yea, a shame to get your own team stranded." ]+ if not b2 then failWith msg2+ else do+ leader <- getLeaderUI+ DebugModeCli{sdifficultyCli} <- getsClient sdebugCli+ ConfigUI{configHeroNames} <- getsClient sconfigUI+ return $ Right $ GameRestartSer leader t sdifficultyCli configHeroNames -- * GameExit; does not take time -gameExitHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSer+gameExitHuman :: MonadClientUI m => m (SlideOrCmd CmdSer) gameExitHuman = do go <- displayYesNo ColorFull "Really save and exit?" if go then do leader <- getLeaderUI- return $ GameExitSer leader- else abortWith "Save and exit canceled."+ DebugModeCli{sdifficultyCli} <- getsClient sdebugCli+ return $ Right $ GameExitSer leader sdifficultyCli+ else failWith "Save and exit canceled." -- * GameSave; does not take time @@ -511,4 +650,4 @@ leader <- getLeaderUI -- TODO: do not save to history: msgAdd "Saving game backup."- return $ GameSaveSer leader+ return $! GameSaveSer leader
Game/LambdaHack/Client/HumanLocal.hs view
@@ -2,29 +2,33 @@ -- server commands. None of such commands takes game time. -- TODO: document module Game.LambdaHack.Client.HumanLocal- ( -- * Semantics of serverl-less human commands- moveCursor, retargetLeader- , selectHeroHuman, memberCycleHuman, memberBackHuman- , inventoryHuman, tgtFloorLeader, tgtEnemyLeader, tgtAscendHuman- , epsIncrHuman, cancelHuman, displayMainMenu, acceptHuman, clearHuman- , historyHuman, humanMarkVision, humanMarkSmell, humanMarkSuspect- , helpHuman- -- * Helper functions useful also elsewhere- , endTargeting, floorItemOverlay, itemOverlay, viewedLevel, selectLeader- , stopRunning, lookAt+ ( -- * Assorted commands that do not notify the server+ gameDifficultyCycle+ , pickLeaderHuman, memberCycleHuman, memberBackHuman, inventoryHuman+ , selectActorHuman, selectNoneHuman, clearHuman, repeatHuman, recordHuman+ , historyHuman, markVisionHuman, markSmellHuman, markSuspectHuman+ , helpHuman, mainMenuHuman, macroHuman+ -- * Commands specific to targeting+ , moveCursorHuman, tgtFloorHuman, tgtEnemyHuman+ , tgtUnknownHuman, tgtItemHuman, tgtStairHuman, tgtAscendHuman+ , epsIncrHuman, tgtClearHuman, cancelHuman, acceptHuman+ -- * Helper definitions+ , floorItemOverlay, itemOverlay+ , pickLeader, lookAt ) where -- Cabal import qualified Paths_LambdaHack as Self (version) +import Control.Exception.Assert.Sugar import Control.Monad-import Control.Monad.Writer.Strict (WriterT, tell) import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Function import Data.List import qualified Data.Map.Strict as M import Data.Maybe+import Data.Monoid import Data.Ord import Data.Text (Text) import qualified Data.Text as T@@ -32,17 +36,17 @@ import Game.LambdaHack.Frontend (frontendName) import qualified NLP.Miniutter.English as MU -import Control.Exception.Assert.Sugar import Game.LambdaHack.Client.Action import Game.LambdaHack.Client.Binding-import qualified Game.LambdaHack.Client.HumanCmd as HumanCmd import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.Animation import qualified Game.LambdaHack.Common.Effect as Effect import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Feature as F+import qualified Game.LambdaHack.Common.HumanCmd as HumanCmd import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Key as K import qualified Game.LambdaHack.Common.Kind as Kind@@ -57,176 +61,54 @@ import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind --- * Move and Run--moveCursor :: MonadClientUI m => Vector -> Int -> WriterT Slideshow m ()-moveCursor dir n = do- leader <- getLeaderUI- lpos <- getsState $ bpos . getActorBody leader- scursor <- getsClient scursor- let cpos = fromMaybe lpos scursor- Level{lxsize, lysize} <- cursorLevel- let shiftB pos = shiftBounded lxsize (1, 1, lxsize - 2, lysize - 2) pos dir- modifyClient $ \cli -> cli {scursor = Just $ iterate shiftB cpos !! n}- doLook--cursorLevel :: MonadClient m => m Level-cursorLevel = do- dungeon <- getsState sdungeon- stgtMode <- getsClient stgtMode- cli <- getClient- let tgtId = maybe (assert `failure` "not targetting right now"- `twith` cli) tgtLevelId stgtMode- return $! dungeon EM.! tgtId--viewedLevel :: MonadClientUI m => m (LevelId, Level)-viewedLevel = do- arena <- getArenaUI- dungeon <- getsState sdungeon- stgtMode <- getsClient stgtMode- let tgtId = maybe arena tgtLevelId stgtMode- return (tgtId, dungeon EM.! tgtId)---- TODO: probably move somewhere (Level?)--- | Produces a textual description of the terrain and items at an already--- explored position. Mute for unknown positions.--- The detailed variant is for use in the targeting mode.-lookAt :: MonadClientUI m- => Bool -- ^ detailed?- -> Bool -- ^ can be seen right now?- -> Point -- ^ position to describe- -> ActorId -- ^ the actor that looks- -> Text -- ^ an extra sentence to print- -> m Text-lookAt detailed canSee pos aid msg = do- Kind.COps{coitem, cotile=cotile@Kind.Ops{okind}} <- getsState scops- (_, lvl) <- viewedLevel- subject <- partAidLeader aid- s <- getState- let is = lvl `atI` pos- verb = MU.Text $ if canSee then "see" else "remember"- disco <- getsClient sdisco- let nWs (iid, k) = partItemWs coitem disco k (getItemBody iid s)- isd = case detailed of- _ | EM.size is == 0 -> ""- _ | EM.size is <= 2 ->- makeSentence [ MU.SubjectVerbSg subject verb- , MU.WWandW $ map nWs $ EM.assocs is]- True -> "Objects:"- _ -> "Objects here."- tile = lvl `at` pos- obscured | tile /= hideTile cotile lvl pos = "partially obscured"- | otherwise = ""- if detailed- then return $! makeSentence [obscured, MU.Text $ tname $ okind tile]- <+> msg <+> isd- else return $! msg <+> isd---- | Perform look around in the current position of the cursor.--- Assumes targeting mode and so assumes that a leader is selected.-doLook :: MonadClientUI m => WriterT Slideshow m ()-doLook = do- scursor <- getsClient scursor- (lid, lvl) <- viewedLevel- per <- getPerFid lid- leader <- getLeaderUI- lpos <- getsState $ bpos . getActorBody leader- target <- getsClient $ getTarget leader- hms <- getsState $ actorList (const True) lid- let p = fromMaybe lpos scursor- canSee = ES.member p (totalVisible per)- ihabitant | canSee = find (\m -> bpos m == p) hms- | otherwise = Nothing- enemyMsg =- -- Even if it's the leader, give his proper name, not 'you'.- maybe "" (\b -> let subject = partActor b- verb = "be here"- in makeSentence [MU.SubjectVerbSg subject verb])- ihabitant- vis | not $ p `ES.member` totalVisible per = " (not visible)"- | actorSeesPos per leader p = ""- | otherwise = " (not visible)"- mode = case target of- Just TEnemy{} -> "[targeting foe" <> vis <> "]"- Just TPos{} -> "[targeting position" <> vis <> "]"- Nothing -> "[targeting current" <> vis <> "]"- -- Check if there's something lying around at current position.- is = lvl `atI` p- -- Show general info about current position.- lookMsg <- lookAt True canSee p leader enemyMsg- modifyClient (\st -> st {slastKey = Nothing})- if EM.size is <= 2 then do- slides <- promptToSlideshow (mode <+> lookMsg)- tell slides- else do- io <- floorItemOverlay is- slides <- overlayToSlideshow (mode <+> lookMsg) io- tell slides---- | Create a list of item names.-floorItemOverlay :: MonadClient m => ItemBag -> m Overlay-floorItemOverlay bag = do- Kind.COps{coitem} <- getsState scops- s <- getState- disco <- getsClient sdisco- let is = zip (EM.assocs bag) (allLetters ++ repeat (InvChar ' '))- pr ((iid, k), l) =- makePhrase [ letterLabel l- , partItemWs coitem disco k (getItemBody iid s) ]- <> " "- return $ map pr is---- | Create a list of item names.-itemOverlay :: MonadClient m => ItemBag -> ItemInv -> m Overlay-itemOverlay bag inv = do- Kind.COps{coitem} <- getsState scops- s <- getState- disco <- getsClient sdisco- let checkItem (l, iid) = fmap (\k -> (l, iid, k)) $ EM.lookup iid bag- is = mapMaybe checkItem $ EM.assocs inv- pr (l, iid, k) =- makePhrase [ letterLabel l- , partItemWs coitem disco k (getItemBody iid s) ]- <> " "- return $ map pr is+failWith :: MonadClientUI m => Msg -> m Slideshow+failWith msg = do+ modifyClient $ \cli -> cli {slastKey = Nothing}+ stopPlayBack+ assert (not $ T.null msg) $ promptToSlideshow msg --- * Project+-- * GameDifficultyCycle -retargetLeader :: MonadClientUI m => WriterT Slideshow m ()-retargetLeader = do- arena <- getArenaUI- -- TODO: do not save to history:- msgAdd "Last target invalid."- modifyClient $ \cli -> cli {scursor = Nothing, seps = 0}- tgtEnemyLeader $ TgtAuto arena+gameDifficultyCycle :: MonadClientUI m => m ()+gameDifficultyCycle = do+ DebugModeCli{sdifficultyCli} <- getsClient sdebugCli+ let d = if sdifficultyCli <= -4 then 4 else sdifficultyCli - 1+ modifyClient $ \cli -> cli {sdebugCli = (sdebugCli cli) {sdifficultyCli = d}}+ msgAdd $ "Next game difficulty set to" <+> tshow (5 - d) <> "." --- * SelectHero+-- * PickLeader -selectHeroHuman :: (MonadClientAbort m, MonadClientUI m) => Int -> m ()-selectHeroHuman k = do+pickLeaderHuman :: MonadClientUI m => Int -> m Slideshow+pickLeaderHuman k = do side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD s <- getState case tryFindHeroK s side k of- Nothing -> abortWith "No such member of the party."- Just (aid, _) -> void $ selectLeader aid+ _ | isSpawnFact fact -> failWith "spawners cannot manually change leaders"+ Nothing -> failWith "No such member of the party."+ Just (aid, _) -> do+ void $ pickLeader aid+ return mempty -- * MemberCycle -- | Switches current member to the next on the level, if any, wrapping.-memberCycleHuman :: (MonadClientAbort m, MonadClientUI m) => m ()+memberCycleHuman :: MonadClientUI m => m Slideshow memberCycleHuman = do+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD leader <- getLeaderUI body <- getsState $ getActorBody leader hs <- partyAfterLeader leader case filter (\(_, b) -> blid b == blid body) hs of- [] -> abortWith "Cannot select any other member on this level."+ _ | isSpawnFact fact -> failWith "spawners cannot manually change leaders"+ [] -> failWith "Cannot pick any other member on this level." (np, b) : _ -> do- success <- selectLeader np+ success <- pickLeader np assert (success `blame` "same leader" `twith` (leader, np, b)) skip+ return mempty -partyAfterLeader :: MonadActionRO m- => ActorId- -> m [(ActorId, Actor)]+partyAfterLeader :: MonadActionRO m => ActorId -> m [(ActorId, Actor)] partyAfterLeader leader = do faction <- getsState $ bfid . getActorBody leader allA <- getsState $ EM.assocs . sactorD@@ -237,49 +119,51 @@ hs = hs9 ++ deleteFirstsBy ((==) `on` fst) factionA hs9 i = fromMaybe (-1) $ findIndex ((== leader) . fst) hs (lt, gt) = (take i hs, drop (i + 1) hs)- return $ gt ++ lt+ return $! gt ++ lt -- | Select a faction leader. False, if nothing to do.-selectLeader :: MonadClientUI m => ActorId -> m Bool-selectLeader actor = do+pickLeader :: MonadClientUI m => ActorId -> m Bool+pickLeader actor = do leader <- getLeaderUI stgtMode <- getsClient stgtMode if leader == actor- then return False -- already selected+ then return False -- already picked else do pbody <- getsState $ getActorBody actor assert (not (bproj pbody) `blame` "projectile chosen as the leader" `twith` (actor, pbody)) skip -- Even if it's already the leader, give his proper name, not 'you'. let subject = partActor pbody- msgAdd $ makeSentence [subject, "selected"]+ msgAdd $ makeSentence [subject, "picked as a leader"] -- Update client state. s <- getState modifyClient $ updateLeader actor s -- Move the cursor, if active, to the new level.- when (isJust stgtMode) $ setTgtId $ blid pbody+ case stgtMode of+ Nothing -> return ()+ Just _ ->+ modifyClient $ \cli -> cli {stgtMode = Just $ TgtMode $ blid pbody} -- Inform about items, etc.- lookMsg <- lookAt False True (bpos pbody) actor ""+ lookMsg <- lookAt False "" True (bpos pbody) actor "" msgAdd lookMsg- -- Don't continue an old run, if any.- stopRunning return True -stopRunning :: MonadClient m => m ()-stopRunning = modifyClient (\ cli -> cli { srunning = Nothing })- -- * MemberBack -- | Switches current member to the previous in the whole dungeon, wrapping.-memberBackHuman :: (MonadClientAbort m, MonadClientUI m) => m ()+memberBackHuman :: MonadClientUI m => m Slideshow memberBackHuman = do+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD leader <- getLeaderUI hs <- partyAfterLeader leader case reverse hs of- [] -> abortWith "No other member in the party."+ _ | isSpawnFact fact -> failWith "spawners cannot manually change leaders"+ [] -> failWith "No other member in the party." (np, b) : _ -> do- success <- selectLeader np+ success <- pickLeader np assert (success `blame` "same leader" `twith` (leader, np, b)) skip+ return mempty -- * Inventory @@ -287,107 +171,482 @@ -- announcing that) and show the inventory of the new leader (unless -- we have just a single inventory in the future). -- | Display inventory-inventoryHuman :: (MonadClientAbort m, MonadClientUI m)- => WriterT Slideshow m ()+inventoryHuman :: MonadClientUI m => m Slideshow inventoryHuman = do leader <- getLeaderUI subject <- partAidLeader leader bag <- getsState $ getActorBag leader- inv <- getsState $ getActorInv leader+ invRaw <- getsState $ getActorInv leader if EM.null bag- then abortWith $ makeSentence+ then promptToSlideshow $ makeSentence [ MU.SubjectVerbSg subject "be" , "not carrying anything" ] else do let blurb = makePhrase [MU.Capitalize $ MU.SubjectVerbSg subject "be carrying:"]+ inv = EM.filter (`EM.member` bag) invRaw io <- itemOverlay bag inv- slides <- overlayToSlideshow blurb io- tell slides+ overlayToSlideshow blurb io --- * TgtFloor+-- * SelectActor --- | Start floor targeting mode or reset the cursor position to the leader.--- Note that the origin of a command (the hero that performs it) is unaffected--- by targeting. For example, not the targeted door, but one adjacent--- to the selected hero is closed by him.-tgtFloorLeader :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()-tgtFloorLeader stgtModeNew = do+-- TODO: make the message (and for selectNoneHuman, pickLeader, etc.)+-- optional, since they have a clear representation in the UI elsewhere.+selectActorHuman ::MonadClientUI m => m Slideshow+selectActorHuman = do+ mleader <- getsClient _sleader+ case mleader of+ Nothing -> failWith "no leader picked, cannot select"+ Just leader -> do+ body <- getsState $ getActorBody leader+ wasMemeber <- getsClient $ ES.member leader . sselected+ let upd = if wasMemeber+ then ES.delete leader -- already selected, deselect instead+ else ES.insert leader+ modifyClient $ \cli -> cli {sselected = upd $ sselected cli}+ let subject = partActor body+ msgAdd $ makeSentence [subject, if wasMemeber+ then "deselected"+ else "selected"]+ return mempty++-- * SelectNone++selectNoneHuman :: (MonadClientUI m, MonadClient m) => m ()+selectNoneHuman = do+ side <- getsClient sside+ lidV <- viewedLevel+ oursAssocs <- getsState $ actorNotProjAssocs (== side) lidV+ let ours = ES.fromList $ map fst oursAssocs+ oldSel <- getsClient sselected+ let wasNone = ES.null $ ES.intersection ours oldSel+ upd = if wasNone+ then ES.union -- already all deselected; select all instead+ else ES.difference+ modifyClient $ \cli -> cli {sselected = upd (sselected cli) ours}+ let subject = "all party members on the level"+ msgAdd $ makeSentence [subject, if wasNone+ then "selected"+ else "deselected"]++-- * Clear++-- | Clear current messages, show the next screen if any.+clearHuman :: Monad m => m ()+clearHuman = return ()++-- * Repeat++-- Note that walk followed by repeat should not be equivalent to run,+-- because the player can really use a command that does not stop+-- at terrain change or when walking over items.+repeatHuman :: MonadClient m => Int -> m ()+repeatHuman n = do+ (_, seqPrevious, k) <- getsClient slastRecord+ let macro = concat $ replicate n $ reverse seqPrevious+ modifyClient $ \cli -> cli {slastPlay = macro ++ slastPlay cli}+ let slastRecord = ([], [], if k == 0 then 0 else maxK)+ modifyClient $ \cli -> cli {slastRecord}++maxK :: Int+maxK = 100++-- * Record++recordHuman :: MonadClientUI m => m Slideshow+recordHuman = do+ modifyClient $ \cli -> cli {slastKey = Nothing}+ (_seqCurrent, seqPrevious, k) <- getsClient slastRecord+ case k of+ 0 -> do+ let slastRecord = ([], [], maxK)+ modifyClient $ \cli -> cli {slastRecord}+ promptToSlideshow $ "Macro will be recorded for up to"+ <+> tshow maxK <+> "steps."+ _ -> do+ let slastRecord = (seqPrevious, [], 0)+ modifyClient $ \cli -> cli {slastRecord}+ promptToSlideshow $ "Macro recording interrupted after"+ <+> tshow (maxK - k - 1) <+> "steps."++-- * History++historyHuman :: MonadClientUI m => m Slideshow+historyHuman = do+ history <- getsClient shistory+ arena <- getArenaUI+ local <- getsState $ getLocalTime arena+ global <- getsState stime+ let msg = makeSentence+ [ "You survived for"+ , MU.CarWs (global `timeFit` timeTurn) "half-second turn"+ , "(this level:"+ , MU.Text (tshow (local `timeFit` timeTurn)) MU.:> ")" ]+ <+> "Past messages:"+ overlayToBlankSlideshow msg $ renderHistory history++-- * MarkVision, MarkSmell, MarkSuspect++markVisionHuman :: MonadClientUI m => m ()+markVisionHuman = do+ modifyClient toggleMarkVision+ cur <- getsClient smarkVision+ msgAdd $ "Visible area display toggled" <+> if cur then "on." else "off."++markSmellHuman :: MonadClientUI m => m ()+markSmellHuman = do+ modifyClient toggleMarkSmell+ cur <- getsClient smarkSmell+ msgAdd $ "Smell display toggled" <+> if cur then "on." else "off."++markSuspectHuman :: MonadClientUI m => m ()+markSuspectHuman = do+ -- BFS takes suspect tiles into account depending on @smarkSuspect@,+ -- so we need to invalidate the BFS data caches.+ modifyClient $ \cli -> cli {sbfsD = EM.empty}+ modifyClient toggleMarkSuspect+ cur <- getsClient smarkSuspect+ msgAdd $ "Suspect terrain display toggled" <+> if cur then "on." else "off."++-- * Help++-- | Display command help.+helpHuman :: MonadClientUI m => m Slideshow+helpHuman = do+ keyb <- askBinding+ return $! keyHelp keyb++-- * MainMenu++-- TODO: merge with the help screens better+-- | Display the main menu.+mainMenuHuman :: MonadClientUI m => m Slideshow+mainMenuHuman = do+ Kind.COps{corule} <- getsState scops+ Binding{brevMap, bcmdList} <- askBinding+ scurDifficulty <- getsClient scurDifficulty+ DebugModeCli{sdifficultyCli} <- getsClient sdebugCli+ let stripFrame t = map (T.tail . T.init) $ tail . init $ T.lines t+ pasteVersion art =+ let pathsVersion = rpathsVersion $ Kind.stdRuleset corule+ version = " Version " ++ showVersion pathsVersion+ ++ " (frontend: " ++ frontendName+ ++ ", engine: LambdaHack " ++ showVersion Self.version+ ++ ") "+ versionLen = length version+ in init art ++ [take (80 - versionLen) (last art) ++ version]+ kds = -- key-description pairs+ let showKD cmd km = (K.showKM km, HumanCmd.cmdDescription cmd)+ revLookup cmd = maybe ("", "") (showKD cmd) $ M.lookup cmd brevMap+ cmds = [ (K.showKM km, desc)+ | (km, (desc, HumanCmd.CmdMenu, cmd)) <- bcmdList,+ cmd /= HumanCmd.GameDifficultyCycle ]+ in [ (fst (revLookup HumanCmd.Cancel), "back to playing")+ , (fst (revLookup HumanCmd.Accept), "see more help") ]+ ++ cmds+ ++ [ (fst ( revLookup HumanCmd.GameDifficultyCycle)+ , "next game difficulty"+ <+> tshow (5 - sdifficultyCli)+ <+> "(current"+ <+> tshow (5 - scurDifficulty) <> ")" ) ]+ bindingLen = 25+ bindings = -- key bindings to display+ let fmt (k, d) = T.justifyLeft bindingLen ' '+ $ T.justifyLeft 7 ' ' k <> " " <> d+ in map fmt kds+ overwrite = -- overwrite the art with key bindings+ 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, T.pack prefix <> binding+ <> T.drop (T.length binding - bindingLen)+ (T.pack suffix))+ else (bs, T.pack line)+ in snd . mapAccumL over bindings+ mainMenuArt = rmainMenuArt $ Kind.stdRuleset corule+ 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" `twith` mainMenuArt+ hd : tl -> overlayToBlankSlideshow hd (toOverlay tl)+ -- TODO: keys don't work if tl/=[]++-- * Macro++macroHuman :: MonadClient m => [String] -> m ()+macroHuman kms =+ modifyClient $ \cli -> cli {slastPlay = map K.mkKM kms ++ slastPlay cli}++-- * MoveCursor++moveCursorHuman :: MonadClientUI m => Vector -> Int -> m Slideshow+moveCursorHuman dir n = do leader <- getLeaderUI- ppos <- getsState (bpos . getActorBody leader)- target <- getsClient $ getTarget leader stgtMode <- getsClient stgtMode- let tgt = case target of- Just (TEnemy _ _) -> Nothing -- forget enemy target, keep the cursor- _ | isJust stgtMode ->- Just (TPos ppos) -- double key press: reset cursor- t -> t -- keep the target from previous targeting session- -- Register that we want to target only positions.- modifyClient $ updateTarget leader (const tgt)- setCursor stgtModeNew+ let lidV = maybe (assert `failure` leader) tgtLevelId stgtMode+ Level{lxsize, lysize} <- getLevel lidV+ lpos <- getsState $ bpos . getActorBody leader+ scursor <- getsClient scursor+ cursorPos <- cursorToPos+ let cpos = fromMaybe lpos cursorPos+ shiftB pos = shiftBounded lxsize lysize pos dir+ newPos = iterate shiftB cpos !! n+ if newPos == cpos then failWith "never mind"+ else do+ let tgt = case scursor of+ TVector{} -> TVector $ displacement lpos newPos+ _ -> TPoint lidV newPos+ modifyClient $ \cli -> cli {scursor = tgt}+ doLook --- | Set, activate and display cursor information.-setCursor :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()-setCursor stgtModeNew = do- stgtModeOld <- getsClient stgtMode- scursorOld <- getsClient scursor- sepsOld <- getsClient seps- scursor <- targetToPos- let seps = if scursor == scursorOld then sepsOld else 0- stgtMode = if isNothing stgtModeOld- then Just stgtModeNew- else stgtModeOld- modifyClient $ \cli2 -> cli2 {scursor, seps, stgtMode}+-- TODO: probably move somewhere (Level?)+-- | Produces a textual description of the terrain and items at an already+-- explored position. Mute for unknown positions.+-- The detailed variant is for use in the targeting mode.+lookAt :: MonadClientUI m+ => Bool -- ^ detailed?+ -> Text -- ^ how to start tile description+ -> Bool -- ^ can be seen right now?+ -> Point -- ^ position to describe+ -> ActorId -- ^ the actor that looks+ -> Text -- ^ an extra sentence to print+ -> m Text+lookAt detailed tilePrefix canSee pos aid msg = do+ Kind.COps{coitem, cotile=cotile@Kind.Ops{okind}} <- getsState scops+ lidV <- viewedLevel+ lvl <- getLevel lidV+ subject <- partAidLeader aid+ s <- getState+ let is = lvl `atI` pos+ verb = MU.Text $ if canSee then "notice" else "remember"+ disco <- getsClient sdisco+ let nWs (iid, k) = partItemWs coitem disco k (getItemBody iid s)+ isd = case detailed of+ _ | EM.size is == 0 -> ""+ _ | EM.size is <= 2 ->+ makeSentence [ MU.SubjectVerbSg subject verb+ , MU.WWandW $ map nWs $ EM.assocs is]+ True -> "Objects:"+ _ -> "Objects here."+ tile = lvl `at` pos+ obscured | tile /= hideTile cotile lvl pos = "partially obscured"+ | otherwise = ""+ tileText = obscured <+> tname (okind tile)+ tilePart | T.null tilePrefix = MU.Text tileText+ | otherwise = MU.AW $ MU.Text tileText+ tileDesc = [MU.Text tilePrefix, tilePart]+ if not (null (Tile.causeEffects cotile tile)) then+ return $! makeSentence ("activable:" : tileDesc)+ <+> msg <+> isd+ else if detailed then+ return $! makeSentence tileDesc+ <+> msg <+> isd+ else return $! msg <+> isd++-- | Perform look around in the current position of the cursor.+-- Assumes targeting mode and so assumes that a leader is picked.+doLook :: MonadClientUI m => m Slideshow+doLook = do+ leader <- getLeaderUI+ stgtMode <- getsClient stgtMode+ let lidV = maybe (assert `failure` leader) tgtLevelId stgtMode+ lvl <- getLevel lidV+ cursorPos <- cursorToPos+ per <- getPerFid lidV+ b <- getsState $ getActorBody leader+ let p = fromMaybe (bpos b) cursorPos+ canSee = ES.member p (totalVisible per)+ inhabitants <- if canSee+ then getsState $ posToActors p lidV+ else return []+ aims <- actorAimsPos leader p+ let enemyMsg = case inhabitants of+ [] -> ""+ _ -> -- Even if it's the leader, give his proper name, not 'you'.+ let subjects = map (partActor . snd . fst) inhabitants+ subject = MU.WWandW subjects+ verb = "be here"+ in makeSentence [MU.SubjectVerbSg subject verb]+ vis | not canSee = "you cannot see"+ | not aims = "you cannot penetrate"+ | otherwise = "you see"+ -- Show general info about current position.+ lookMsg <- lookAt True vis canSee p leader enemyMsg+ modifyClient $ \cli -> cli {slastKey = Nothing}+ -- Check if there's something lying around at current position.+ let is = lvl `atI` p+ if EM.size is <= 2 then+ promptToSlideshow lookMsg+ else do+ io <- floorItemOverlay is+ overlayToSlideshow lookMsg io++-- | Create a list of item names.+floorItemOverlay :: MonadClient m => ItemBag -> m Overlay+floorItemOverlay bag = do+ Kind.COps{coitem} <- getsState scops+ s <- getState+ disco <- getsClient sdisco+ let is = zip (EM.assocs bag) (allLetters ++ repeat (InvChar ' '))+ pr ((iid, k), l) =+ makePhrase [ letterLabel l+ , partItemWs coitem disco k (getItemBody iid s) ]+ <> " "+ return $! toOverlay $ map pr is++-- | Create a list of item names.+itemOverlay :: MonadClient m => ItemBag -> ItemInv -> m Overlay+itemOverlay bag inv = do+ Kind.COps{coitem} <- getsState scops+ s <- getState+ disco <- getsClient sdisco+ let pr (l, iid) =+ makePhrase [ letterLabel l+ , partItemWs coitem disco (bag EM.! iid)+ (getItemBody iid s) ]+ <> " "+ return $! toOverlay $ map pr $ EM.assocs inv++-- * TgtFloor++-- | Cycle targeting mode. Do not change position of the cursor,+-- switch among things at that position.+tgtFloorHuman :: MonadClientUI m => m Slideshow+tgtFloorHuman = do+ lidV <- viewedLevel+ leader <- getLeaderUI+ lpos <- getsState $ bpos . getActorBody leader+ cursorPos <- cursorToPos+ scursor <- getsClient scursor+ stgtMode <- getsClient stgtMode+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ bfs <- getCacheBfs leader+ bsAll <- getsState $ actorAssocs (const True) lidV+ let cursor = fromMaybe lpos cursorPos+ tgt = case scursor of+ _ | isNothing stgtMode -> -- first key press: keep target+ scursor+ TEnemy a False -> TEnemy a True+ TEnemy{} -> TPoint lidV cursor+ TEnemyPos{} -> TPoint lidV cursor+ TPoint{} -> TVector $ displacement lpos cursor+ TVector{} ->+ let isEnemy b = isAtWar fact (bfid b)+ && not (bproj b)+ && posAimsPos bfs lpos (bpos b)+ -- For projectiles, we pick here the first that would be picked+ -- by '*', so that all other projectiles on the tile come next,+ -- without any intervening actors from other tiles.+ in case find (\(_, m) -> Just (bpos m) == cursorPos) bsAll of+ Just (im, m) | isEnemy m -> TEnemy im False+ Just (im, _) -> TEnemy im True+ Nothing -> TPoint lidV cursor+ modifyClient $ \cli -> cli {scursor = tgt, stgtMode = Just $ TgtMode lidV} doLook -- * TgtEnemy --- | Start the enemy targeting mode. Cycle between enemy targets.-tgtEnemyLeader :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()-tgtEnemyLeader stgtModeNew = do+tgtEnemyHuman :: MonadClientUI m => m Slideshow+tgtEnemyHuman = do+ lidV <- viewedLevel leader <- getLeaderUI- ppos <- getsState (bpos . getActorBody leader)- (lid, Level{lxsize}) <- viewedLevel- per <- getPerFid lid- target <- getsClient $ getTarget leader+ lpos <- getsState $ bpos . getActorBody leader+ cursorPos <- cursorToPos+ scursor <- getsClient scursor stgtMode <- getsClient stgtMode side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD- bs <- getsState $ actorNotProjAssocs (isAtWar fact) lid- let ordPos (_, b) = (chessDist lxsize ppos $ bpos b, bpos b)- dbs = sortBy (comparing ordPos) bs- (lt, gt) = case target of- Just (TEnemy n _) | isJust stgtMode -> -- pick next enemy- let i = fromMaybe (-1) $ findIndex ((== n) . fst) dbs- in splitAt (i + 1) dbs- Just (TEnemy n _) -> -- try to retarget the old enemy- let i = fromMaybe (-1) $ findIndex ((== n) . fst) dbs- in splitAt i dbs- _ -> (dbs, []) -- target first enemy (e.g., number 0)+ bfs <- getCacheBfs leader+ bsAll <- getsState $ actorAssocs (const True) lidV+ let ordPos (_, b) = (chessDist lpos $ bpos b, bpos b)+ dbs = sortBy (comparing ordPos) bsAll+ pickUnderCursor = -- switch to the enemy under cursor, if any+ let i = fromMaybe (-1)+ $ findIndex ((== cursorPos) . Just . bpos . snd) dbs+ in splitAt i dbs+ (permitAnyActor, (lt, gt)) = case scursor of+ TEnemy a permit | isJust stgtMode -> -- pick next enemy+ let i = fromMaybe (-1) $ findIndex ((== a) . fst) dbs+ in (permit, splitAt (i + 1) dbs)+ TEnemy a permit -> -- first key press, retarget old enemy+ let i = fromMaybe (-1) $ findIndex ((== a) . fst) dbs+ in (permit, splitAt i dbs)+ TEnemyPos _ _ _ permit -> (permit, pickUnderCursor)+ _ -> (False, pickUnderCursor) -- the sensible default is only-foes gtlt = gt ++ lt- seen (_, b) =- let mpos = bpos b -- it is remembered by faction- in actorSeesPos per leader mpos -- is it visible by actor?- lf = filter seen gtlt- tgt = case lf of- [] -> target -- no enemies in sight, stick to last target- (na, nm) : _ -> Just (TEnemy na (bpos nm)) -- pick the next+ isEnemy b = isAtWar fact (bfid b)+ && not (bproj b)+ && posAimsPos bfs lpos (bpos b)+ lf = filter (isEnemy . snd) gtlt+ tgt | permitAnyActor = case gtlt of+ (a, _) : _ -> TEnemy a True+ [] -> scursor -- no actors in sight, stick to last target+ | otherwise = case lf of+ (a, _) : _ -> TEnemy a False+ [] -> scursor -- no seen foes in sight, stick to last target -- Register the chosen enemy, to pick another on next invocation.- modifyClient $ updateTarget leader (const tgt)- setCursor stgtModeNew+ modifyClient $ \cli -> cli {scursor = tgt, stgtMode = Just $ TgtMode lidV}+ doLook +-- * TgtUnknown++tgtUnknownHuman :: MonadClientUI m => m Slideshow+tgtUnknownHuman = do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ mpos <- closestUnknown leader+ case mpos of+ Nothing -> failWith "no more unknown spots left"+ Just p -> do+ let tgt = TPoint (blid b) p+ modifyClient $ \cli -> cli {scursor = tgt}+ return mempty++-- * TgtItem++tgtItemHuman :: MonadClientUI m => m Slideshow+tgtItemHuman = do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ items <- closestItems leader+ case items of+ [] -> failWith "no more items remembered or visible"+ (_, (p, _)) : _ -> do+ let tgt = TPoint (blid b) p+ modifyClient $ \cli -> cli {scursor = tgt}+ return mempty++-- * TgtStair++tgtStairHuman :: MonadClientUI m => Bool -> m Slideshow+tgtStairHuman up = do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ stairs <- closestTriggers (Just up) False leader+ case stairs of+ [] -> failWith $ "no stairs"+ <+> if up then "up" else "down"+ p : _ -> do+ let tgt = TPoint (blid b) p+ modifyClient $ \cli -> cli {scursor = tgt}+ return mempty+ -- * TgtAscend -- | Change the displayed level in targeting mode to (at most) -- k levels shallower. Enters targeting mode, if not already in one.-tgtAscendHuman :: (MonadClientAbort m, MonadClientUI m)- => Int -> WriterT Slideshow m ()+tgtAscendHuman :: MonadClientUI m => Int -> m Slideshow tgtAscendHuman k = do- Kind.COps{cotile} <- getsState scops+ Kind.COps{cotile=cotile@Kind.Ops{okind}} <- getsState scops dungeon <- getsState sdungeon- cursor <- getsClient scursor- (tgtId, lvl) <- viewedLevel- let rightStairs = case cursor of+ scursorOld <- getsClient scursor+ cursorPos <- cursorToPos+ lidV <- viewedLevel+ lvl <- getLevel lidV+ let rightStairs = case cursorPos of Nothing -> Nothing Just cpos -> let tile = lvl `at` cpos@@ -396,213 +655,104 @@ else Nothing case rightStairs of Just cpos -> do -- stairs, in the right direction- (nln, npos) <- getsState $ whereTo tgtId cpos k- assert (nln /= tgtId `blame` "stairs looped" `twith` nln) skip+ (nln, npos) <- getsState $ whereTo lidV cpos k . sdungeon+ assert (nln /= lidV `blame` "stairs looped" `twith` nln) skip -- Do not freely reveal the other end of the stairs.- let scursor =- if Tile.hasFeature cotile F.Exit (lvl `at` npos)- then Just npos -- already know as an exit, focus on it- else cursor -- unknown, do not reveal- modifyClient $ \cli -> cli {scursor}- setTgtId nln+ let ascDesc (F.Cause (Effect.Ascend _)) = True+ ascDesc _ = False+ scursor =+ if any ascDesc $ tfeature $ okind (lvl `at` npos)+ then TPoint nln npos -- already known as an exit, focus on it+ else scursorOld -- unknown, do not reveal+ modifyClient $ \cli -> cli {scursor, stgtMode = Just (TgtMode nln)}+ doLook Nothing -> -- no stairs in the right direction- case ascendInBranch dungeon tgtId k of- [] -> abortWith "no more levels in this direction"- nln : _ -> setTgtId nln- doLook--setTgtId :: MonadClient m => LevelId -> m ()-setTgtId nln = do- stgtMode <- getsClient stgtMode- case stgtMode of- Just (TgtAuto _) ->- modifyClient $ \cli -> cli {stgtMode = Just (TgtAuto nln)}- _ ->- modifyClient $ \cli -> cli {stgtMode = Just (TgtExplicit nln)}+ case ascendInBranch dungeon k lidV of+ [] -> failWith "no more levels in this direction"+ nln : _ -> do+ modifyClient $ \cli -> cli {stgtMode = Just (TgtMode nln)}+ doLook -- * EpsIncr -- | Tweak the @eps@ parameter of the targeting digital line.-epsIncrHuman :: MonadClientAbort m => Bool -> m ()+epsIncrHuman :: MonadClientUI m => Bool -> m Slideshow epsIncrHuman b = do stgtMode <- getsClient stgtMode if isJust stgtMode- then modifyClient $ \cli -> cli {seps = seps cli + if b then 1 else -1}- else neverMind True -- no visual feedback, so no sense+ then do+ modifyClient $ \cli -> cli {seps = seps cli + if b then 1 else -1}+ return mempty+ else failWith "never mind" -- no visual feedback, so no sense +-- * TgtClear++tgtClearHuman :: MonadClient m => m ()+tgtClearHuman = do+ mleader <- getsClient _sleader+ case mleader of+ Nothing -> return ()+ Just leader -> do+ tgt <- getsClient $ getTarget leader+ case tgt of+ Just _ -> modifyClient $ updateTarget leader (const Nothing)+ Nothing -> do+ scursorOld <- getsClient scursor+ b <- getsState $ getActorBody leader+ let scursor = case scursorOld of+ TEnemy _ permit -> TEnemy leader permit+ TEnemyPos _ _ _ permit -> TEnemy leader permit+ TPoint{} -> TPoint (blid b) (bpos b)+ TVector{} -> TVector (Vector 0 0)+ modifyClient $ \cli -> cli {scursor}+ -- * Cancel -- | Cancel something, e.g., targeting mode, resetting the cursor -- to the position of the leader. Chosen target is not invalidated.-cancelHuman :: MonadClientUI m- => WriterT Slideshow m () -> WriterT Slideshow m ()+cancelHuman :: MonadClientUI m => m Slideshow -> m Slideshow cancelHuman h = do stgtMode <- getsClient stgtMode if isJust stgtMode- then endTargeting False+ then targetReject else h -- nothing to cancel right now, treat this as a command invocation --- TODO: merge with the help screens better--- | Display the main menu.-displayMainMenu :: MonadClientUI m => WriterT Slideshow m ()-displayMainMenu = do- Kind.COps{corule} <- getsState scops- Binding{krevMap} <- askBinding- let stripFrame t = map (T.tail . T.init) $ tail . init $ T.lines t- pasteVersion art =- let pathsVersion = rpathsVersion $ Kind.stdRuleset corule- version = " Version " ++ showVersion pathsVersion- ++ " (frontend: " ++ frontendName- ++ ", engine: LambdaHack " ++ showVersion Self.version- ++ ") "- versionLen = length version- in init art ++ [take (80 - versionLen) (last art) ++ version]- kds = -- key-description pairs- let showKD cmd km = (K.showKM km, HumanCmd.cmdDescription cmd)- revLookup cmd = maybe ("", "") (showKD cmd) $ M.lookup cmd krevMap- cmds = [ HumanCmd.GameExit- , HumanCmd.GameRestart "campaign"- , HumanCmd.GameRestart "skirmish"- , HumanCmd.GameRestart "PvP"- , HumanCmd.GameRestart "Coop"- , HumanCmd.GameRestart "defense"- ]- in [ (fst (revLookup HumanCmd.Cancel), "back to playing")- , (fst (revLookup HumanCmd.Accept), "see more help") ]- ++ map revLookup cmds- bindings = -- key bindings to display- let bindingLen = 25- fmt (k, d) =- 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 = ([], T.pack line)- over bs@(binding : bsRest) line =- let (prefix, lineRest) = break (=='{') line- (braces, suffix) = span (=='{') lineRest- in if length braces == 25- then (bsRest, T.pack prefix <> binding <> T.pack suffix)- else (bs, T.pack line)- in snd . mapAccumL over bindings- mainMenuArt = rmainMenuArt $ Kind.stdRuleset corule- 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" `twith` mainMenuArt- hd : tl -> do- slides <- overlayToSlideshow hd tl -- TODO: keys don't work if tl/=[]- tell slides+-- | End targeting mode, rejecting the current position.+targetReject :: MonadClientUI m => m Slideshow+targetReject = do+ modifyClient $ \cli -> cli {stgtMode = Nothing}+ failWith "targeting canceled" -- * Accept -- | Accept something, e.g., targeting mode, keeping cursor where it was. -- Or perform the default action, if nothing needs accepting.-acceptHuman :: MonadClientUI m- => WriterT Slideshow m () -> WriterT Slideshow m ()+acceptHuman :: MonadClientUI m => m Slideshow -> m Slideshow acceptHuman h = do stgtMode <- getsClient stgtMode if isJust stgtMode- then endTargeting True+ then do+ targetAccept+ return mempty else h -- nothing to accept right now, treat this as a command invocation --- | End targeting mode, accepting the current position or not.-endTargeting :: MonadClientUI m => Bool -> m ()-endTargeting accept = do- when accept $ do- leader <- getLeaderUI- target <- getsClient $ getTarget leader- scursor <- getsClient scursor- (lid, _) <- viewedLevel- side <- getsClient sside- fact <- getsState $ (EM.! side) . sfactionD- bs <- getsState $ actorNotProjAssocs (isAtWar fact) lid- case target of- Just TEnemy{} ->- -- If in enemy targeting mode, switch to the enemy under- -- the current cursor position, if any.- case find (\ (_im, m) -> Just (bpos m) == scursor) bs of- Just (im, m) ->- let tgt = Just $ TEnemy im (bpos m)- in modifyClient $ updateTarget leader (const tgt)- Nothing -> return ()- _ -> case scursor of- Nothing -> return ()- Just cpos ->- modifyClient $ updateTarget leader (const $ Just $ TPos cpos)- if accept- then endTargetingMsg- -- TODO: do not save to history:- else msgAdd "targeting canceled"- modifyClient $ \cli -> cli { stgtMode = Nothing }+-- | End targeting mode, accepting the current position.+targetAccept :: MonadClientUI m => m ()+targetAccept = do+ endTargeting+ endTargetingMsg+ modifyClient $ \cli -> cli {stgtMode = Nothing} +-- | End targeting mode, accepting the current position.+endTargeting :: MonadClientUI m => m ()+endTargeting = do+ leader <- getLeaderUI+ scursor <- getsClient scursor+ modifyClient $ updateTarget leader $ const $ Just scursor+ endTargetingMsg :: MonadClientUI m => m () endTargetingMsg = do leader <- getLeaderUI- pbody <- getsState $ getActorBody leader- target <- getsClient $ getTarget leader- s <- getState- Level{lxsize} <- cursorLevel- let targetMsg = case target of- Just (TEnemy a _ll) ->- if memActor a (blid pbody) s- -- Never equal to leader, hence @partActor@.- then partActor $ getActorBody a s- else "a fear of the past"- Just (TPos tpos) ->- MU.Text $ "position" <+> showPoint lxsize tpos- Nothing -> "current cursor position continuously"+ targetMsg <- targetDescLeader leader subject <- partAidLeader leader- msgAdd $ makeSentence [MU.SubjectVerbSg subject "target", targetMsg]---- * Clear---- | Clear current messages, show the next screen if any.-clearHuman :: Monad m => m ()-clearHuman = return ()---- * History--historyHuman :: MonadClientUI m => WriterT Slideshow m ()-historyHuman = do- history <- getsClient shistory- arena <- getArenaUI- local <- getsState $ getLocalTime arena- global <- getsState stime- let msg = makeSentence- [ "You survived for"- , MU.CarWs (global `timeFit` timeTurn) "half-second turn"- , "(this level:"- , MU.Text (showT (local `timeFit` timeTurn)) MU.:> ")" ]- <+> "Past messages:"- slides <- overlayToSlideshow msg $ renderHistory history- tell slides---- * MarkVision, MarkSmell, MarkSuspect--humanMarkVision :: MonadClientUI m => m ()-humanMarkVision = do- modifyClient toggleMarkVision- cur <- getsClient smarkVision- msgAdd $ "Visible area display toggled" <+> if cur then "on." else "off."--humanMarkSmell :: MonadClientUI m => m ()-humanMarkSmell = do- modifyClient toggleMarkSmell- cur <- getsClient smarkSmell- msgAdd $ "Smell display toggled" <+> if cur then "on." else "off."--humanMarkSuspect :: MonadClientUI m => m ()-humanMarkSuspect = do- modifyClient toggleMarkSuspect- cur <- getsClient smarkSuspect- msgAdd $ "Suspect terrain display toggled" <+> if cur then "on." else "off."---- * Help---- | Display command help.-helpHuman :: MonadClientUI m => WriterT Slideshow m ()-helpHuman = do- keyb <- askBinding- tell $ keyHelp keyb+ msgAdd $ makeSentence [MU.SubjectVerbSg subject "target", MU.Text targetMsg]
Game/LambdaHack/Client/HumanSem.hs view
@@ -3,137 +3,79 @@ ( cmdHumanSem ) where -import Control.Monad-import Control.Monad.Writer.Strict (WriterT)-import Data.Maybe+import Data.Monoid -import Control.Exception.Assert.Sugar import Game.LambdaHack.Client.Action-import Game.LambdaHack.Client.HumanCmd import Game.LambdaHack.Client.HumanGlobal import Game.LambdaHack.Client.HumanLocal-import Game.LambdaHack.Client.State-import Game.LambdaHack.Common.Action-import Game.LambdaHack.Common.Actor-import Game.LambdaHack.Common.ActorState-import Game.LambdaHack.Common.Level-import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.HumanCmd import Game.LambdaHack.Common.ServerCmd-import Game.LambdaHack.Common.Vector-import Game.LambdaHack.Common.VectorXY -- | The semantics of human player commands in terms of the @Action@ monad. -- Decides if the action takes time and what action to perform.--- Time cosuming commands are marked as such in help and cannot be+-- Some time cosuming commands are enabled in targeting mode, but cannot be -- invoked in targeting mode on a remote level (level different than--- the level of the selected hero).-cmdHumanSem :: (MonadClientAbort m, MonadClientUI m)- => HumanCmd -> WriterT Slideshow m (Maybe CmdSer)+-- the level of the leader).+cmdHumanSem :: MonadClientUI m => HumanCmd -> m (SlideOrCmd CmdSer) cmdHumanSem cmd = do- arena <- getArenaUI- when (noRemoteHumanCmd cmd) $ checkCursor arena- cmdAction cmd+ if noRemoteHumanCmd cmd then do+ -- If in targeting mode, check if the current level is the same+ -- as player level and refuse performing the action otherwise.+ arena <- getArenaUI+ lidV <- viewedLevel+ if (arena /= lidV) then+ failWith $ "command disabled on a remote level, press ESC to switch back"+ else cmdAction cmd+ else cmdAction cmd --- | The basic action for a command and whether it takes time.-cmdAction :: (MonadClientAbort m, MonadClientUI m)- => HumanCmd -> WriterT Slideshow m (Maybe CmdSer)+-- | Compute the basic action for a command and mark whether it takes time.+cmdAction :: MonadClientUI m => HumanCmd -> m (SlideOrCmd CmdSer) cmdAction cmd = case cmd of- Move v -> moveRunHuman False v- Run v -> moveRunHuman True v- Wait -> fmap (Just . TakeTimeSer) waitHuman- Pickup -> fmap (Just . TakeTimeSer) pickupHuman- Drop -> fmap (Just . TakeTimeSer) dropHuman- Project ts -> projectHuman ts- Apply ts -> fmap (Just . TakeTimeSer) $ applyHuman ts- AlterDir ts -> fmap (Just . TakeTimeSer) $ alterDirHuman ts- TriggerTile ts -> fmap (Just . TakeTimeSer) $ triggerTileHuman ts-- GameRestart t -> fmap Just $ gameRestartHuman t- GameExit -> fmap Just gameExitHuman- GameSave -> fmap Just gameSaveHuman-- SelectHero k -> selectHeroHuman k >> return Nothing- MemberCycle -> memberCycleHuman >> return Nothing- MemberBack -> memberBackHuman >> return Nothing- Inventory -> inventoryHuman >> return Nothing- TgtFloor -> tgtFloorHuman- TgtEnemy -> tgtEnemyHuman- TgtAscend k -> tgtAscendHuman k >> return Nothing- EpsIncr b -> epsIncrHuman b >> return Nothing- Cancel -> cancelHuman displayMainMenu >> return Nothing- Accept -> acceptHuman helpHuman >> return Nothing- Clear -> clearHuman >> return Nothing- History -> historyHuman >> return Nothing- MarkVision -> humanMarkVision >> return Nothing- MarkSmell -> humanMarkSmell >> return Nothing- MarkSuspect -> humanMarkSuspect >> return Nothing- Help -> displayMainMenu >> return Nothing---- | If in targeting mode, check if the current level is the same--- as player level and refuse performing the action otherwise.-checkCursor :: (MonadClientAbort m, MonadClientUI m) => LevelId -> m ()-checkCursor arena = do- (lid, _) <- viewedLevel- when (arena /= lid) $- abortWith "[targeting] command disabled on a remote level, press ESC to switch back"+ Move v -> fmap (fmap CmdTakeTimeSer) $ moveRunHuman False v+ Run v -> fmap (fmap CmdTakeTimeSer) $ moveRunHuman True v+ Wait -> fmap Right $ fmap CmdTakeTimeSer waitHuman+ Pickup -> fmap (fmap CmdTakeTimeSer) pickupHuman+ Drop -> fmap (fmap CmdTakeTimeSer) dropHuman+ Project ts -> fmap (fmap CmdTakeTimeSer) $ projectHuman ts+ Apply ts -> fmap (fmap CmdTakeTimeSer) $ applyHuman ts+ AlterDir ts -> fmap (fmap CmdTakeTimeSer) $ alterDirHuman ts+ TriggerTile ts -> fmap (fmap CmdTakeTimeSer) $ triggerTileHuman ts+ StepToTarget -> fmap (fmap CmdTakeTimeSer) stepToTargetHuman+ Resend -> fmap (fmap CmdTakeTimeSer) resendHuman -moveRunHuman :: (MonadClientAbort m, MonadClientUI m)- => Bool -> VectorXY -> WriterT Slideshow m (Maybe CmdSer)-moveRunHuman run v = do- tgtMode <- getsClient stgtMode- (arena, Level{lxsize}) <- viewedLevel- source <- getLeaderUI- sb <- getsState $ getActorBody source- let dir = toDir lxsize v- if isJust tgtMode then do- moveCursor dir (if run then 10 else 1) >> return Nothing- else do- let tpos = bpos sb `shift` dir- -- We start by checking actors at the the target position,- -- which gives a partial information (actors can be invisible),- -- as opposed to accessibility (and items) which are always accurate- -- (tiles can't be invisible).- tgt <- getsState $ posToActor tpos arena- case tgt of- Nothing -> do -- move or search or alter- when run $ modifyClient $ \cli -> cli {srunning = Just (dir, 1)}- fmap (Just . TakeTimeSer) $ moveRunAid source dir- -- When running, the invisible actor is hit (not displaced!),- -- so that running in the presence of roving invisible- -- actors is equivalent to moving (with visible actors- -- this is not a problem, since runnning stops early enough).- -- TODO: stop running at invisible actor- Just target | run ->- -- Displacing requires accessibility, but it's checked later on.- fmap (Just . TakeTimeSer) $ displaceAid source target- Just target -> do- tb <- getsState $ getActorBody target- -- We always see actors from our own faction.- if bfid tb == bfid sb && not (bproj tb) then do- -- Select adjacent actor by bumping into him. Takes no time.- success <- selectLeader target- assert (success `blame` "bump self" `twith` (source, target, tb)) skip- return Nothing- else- -- Attacking does not require full access, adjacency is enough.- fmap (Just . TakeTimeSer) $ meleeAid source target+ GameRestart t -> gameRestartHuman t+ GameExit -> gameExitHuman+ GameSave -> fmap Right gameSaveHuman -projectHuman :: (MonadClientAbort m, MonadClientUI m)- => [Trigger] -> WriterT Slideshow m (Maybe CmdSer)-projectHuman ts = do- tgtLoc <- targetToPos- if isNothing tgtLoc- then retargetLeader >> return Nothing- else do- leader <- getLeaderUI- fmap (Just . TakeTimeSer) $ projectAid leader ts+ GameDifficultyCycle -> addNoSlides gameDifficultyCycle+ PickLeader k -> fmap Left $ pickLeaderHuman k+ MemberCycle -> fmap Left memberCycleHuman+ MemberBack -> fmap Left memberBackHuman+ Inventory -> fmap Left inventoryHuman+ SelectActor -> fmap Left selectActorHuman+ SelectNone -> addNoSlides selectNoneHuman+ Clear -> addNoSlides clearHuman+ Repeat n -> addNoSlides $ repeatHuman n+ Record -> fmap Left recordHuman+ History -> fmap Left historyHuman+ MarkVision -> addNoSlides markVisionHuman+ MarkSmell -> addNoSlides markSmellHuman+ MarkSuspect -> addNoSlides markSuspectHuman+ Help -> fmap Left helpHuman+ MainMenu -> fmap Left mainMenuHuman+ Macro _ kms -> addNoSlides $ macroHuman kms -tgtFloorHuman :: MonadClientUI m => WriterT Slideshow m (Maybe CmdSer)-tgtFloorHuman = do- arena <- getArenaUI- tgtFloorLeader (TgtExplicit arena) >> return Nothing+ MoveCursor v k -> fmap Left $ moveCursorHuman v k+ TgtFloor -> fmap Left tgtFloorHuman+ TgtEnemy -> fmap Left tgtEnemyHuman+ TgtUnknown -> fmap Left tgtUnknownHuman+ TgtItem -> fmap Left tgtItemHuman+ TgtStair up -> fmap Left $ tgtStairHuman up+ TgtAscend k -> fmap Left $ tgtAscendHuman k+ EpsIncr b -> fmap Left $ epsIncrHuman b+ TgtClear -> addNoSlides tgtClearHuman+ Cancel -> fmap Left $ cancelHuman mainMenuHuman+ Accept -> fmap Left $ acceptHuman helpHuman -tgtEnemyHuman :: MonadClientUI m => WriterT Slideshow m (Maybe CmdSer)-tgtEnemyHuman = do- arena <- getArenaUI- tgtEnemyLeader (TgtExplicit arena) >> return Nothing+addNoSlides :: Monad m => m () -> m (SlideOrCmd CmdSer)+addNoSlides cmdCli = cmdCli >> return (Left mempty)
Game/LambdaHack/Client/LoopAction.hs view
@@ -3,6 +3,7 @@ -- moves turn by turn. module Game.LambdaHack.Client.LoopAction (loopAI, loopUI) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.Text as T @@ -16,20 +17,20 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.State import Game.LambdaHack.Content.RuleKind-import Control.Exception.Assert.Sugar initCli :: MonadClient m => DebugModeCli -> (State -> m ()) -> m Bool initCli sdebugCli putSt = do -- Warning: state and client state are invalid here, e.g., sdungeon -- and sper are empty. cops <- getsState scops+ sconfigUI <- getsClient sconfigUI -- config from file, not savegame modifyClient $ \cli -> cli {sdebugCli} restored <- restoreGame case restored of Just (s, cli) | not $ snewGameCli sdebugCli -> do -- Restore the game. let sCops = updateCOps (const cops) s putSt sCops- putClient cli {sdebugCli}+ putClient cli {sdebugCli, sconfigUI} return True _ -> -- First visit ever, use the initial state. return False@@ -43,19 +44,19 @@ cmd1 <- readServer case (restored, cmd1) of (True, CmdAtomicAI ResumeA{}) -> return ()- (True, CmdAtomicAI RestartA{}) -> return () -- ignoring old savefile+ (True, CmdAtomicAI RestartA{}) -> return () (False, CmdAtomicAI ResumeA{}) -> do removeServerSave error $ T.unpack $- "Savefile of client" <+> showT side+ "Savefile of client" <+> tshow side <+> "not usable. Removing server savefile. Please restart now." (False, CmdAtomicAI RestartA{}) -> return () _ -> assert `failure` "unexpected command" `twith` (side, restored, cmd1) cmdClientAISem cmd1 -- State and client state now valid.- debugPrint $ "AI client" <+> showT side <+> "started."+ debugPrint $ "AI client" <+> tshow side <+> "started." loop- debugPrint $ "AI client" <+> showT side <+> "stopped."+ debugPrint $ "AI client" <+> tshow side <+> "stopped." where loop = do cmd <- readServer@@ -84,7 +85,7 @@ (False, CmdAtomicUI ResumeA{}) -> do removeServerSave error $ T.unpack $- "Savefile of client" <+> showT side+ "Savefile of client" <+> tshow side <+> "not usable. Removing server savefile. Please restart now." (False, CmdAtomicUI RestartA{}) -> do let msg = "Welcome to" <+> title <> "!"@@ -92,9 +93,9 @@ msgAdd msg _ -> assert `failure` "unexpected command" `twith` (side, restored, cmd1) -- State and client state now valid.- debugPrint $ "UI client" <+> showT side <+> "started."+ debugPrint $ "UI client" <+> tshow side <+> "started." loop- debugPrint $ "UI client" <+> showT side <+> "stopped."+ debugPrint $ "UI client" <+> tshow side <+> "stopped." where loop = do cmd <- readServer
Game/LambdaHack/Client/RunAction.hs view
@@ -1,211 +1,276 @@+{-# LANGUAGE RankNTypes #-} -- | Running and disturbance.+--+-- The general rule is: whatever is behind you (and so ignored previously),+-- determines what you ignore moving forward. This is calcaulated+-- separately for the tiles to the left, to the right and in the middle+-- along the running direction. So, if you want to ignore something+-- start running when you stand on it (or to the right or left, respectively)+-- or by entering it (or passing to the right or left, respectively).+--+-- Some things are never ignored, such as: enemies seen, imporant messages+-- heard, solid tiles and actors in the way. module Game.LambdaHack.Client.RunAction- ( continueRunDir+ ( continueRun, moveRunAid ) where +import Control.Exception.Assert.Sugar+import Control.Monad import qualified Data.ByteString.Char8 as BS import qualified Data.EnumMap.Strict as EM-import qualified Data.EnumSet as ES-import qualified Data.List as L+import Data.Function+import Data.List import Data.Maybe -import Control.Exception.Assert.Sugar import Game.LambdaHack.Client.Action import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState-import Game.LambdaHack.Common.Faction-import qualified Game.LambdaHack.Common.Feature as F import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Msg-import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.ServerCmd import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.TileKind --- | Start running in the given direction and with the given number--- of tiles already traversed (usually 0). The first turn of running--- succeeds much more often than subsequent turns, because most--- of the disturbances are ignored, since the player is aware of them--- and still explicitly requests a run.-canRun :: MonadClient m => ActorId -> (Vector, Int) -> m Bool-canRun leader (dir, dist) = do- cops <- getsState scops- b <- getsState $ getActorBody leader- lvl <- getLevel $ blid b- stgtMode <- getsClient stgtMode- assert (isNothing stgtMode `blame` "attempt to run in target mode"- `twith` (dir, dist, stgtMode)) skip- return $ accessibleDir cops lvl (bpos b) dir--runDir :: MonadClient m => ActorId -> (Vector, Int) -> m (Vector, Int)-runDir leader (dir, dist) = do- canR <- canRun leader (dir, dist)- let -- Do not count distance if we just open a door.- distNew = if canR then dist + 1 else dist- return (dir, distNew)---- | Human running mode, determined from the nearby cave layout.-data RunMode =- RunOpen -- ^ open space, in particular the T crossing- | RunHub -- ^ a hub of separate corridors- | RunCorridor !(Vector, Bool) -- ^ a single corridor, turning here or not- | RunDeadEnd -- ^ dead end---- | Determine the running mode. For corridors, pick the running direction--- trying to explore all corners, by prefering cardinal to diagonal moves.-runMode :: Point -> Vector -> (Point -> Vector -> Bool) -> X -> RunMode-runMode pos dir dirEnterable lxsize =- let dirNearby dir1 dir2 = euclidDistSq lxsize dir1 dir2 == 1- dirBackward d = euclidDistSq lxsize (neg dir) d <= 1- dirAhead d = euclidDistSq lxsize dir d <= 2- findOpen =- let f dirC open = open ++- case L.filter (dirNearby dirC) dirsEnterable of- l | dirBackward dirC -> dirC : l -- points backwards- [] -> [] -- a narrow corridor, just one tile wide- [_] -> [] -- a turning corridor, two tiles wide- l -> dirC : l -- too wide- in L.foldr f []- dirsEnterable = L.filter (dirEnterable pos) (moves lxsize)- in case dirsEnterable of- [] -> assert `failure` "actor is stuck" `twith` (pos, dir) -- TODO- [negdir] -> assert (negdir == neg dir) RunDeadEnd- _ ->- let dirsOpen = findOpen dirsEnterable- dirsCorridor = dirsEnterable L.\\ dirsOpen- in case dirsCorridor of- [] -> RunOpen -- no corridors- _ | L.any dirAhead dirsOpen -> RunOpen -- open space ahead- [d] -> RunCorridor (d, False) -- corridor with no turn- [d1, d2] | dirNearby d1 d2 -> -- corridor with a turn- -- Prefer cardinal to diagonal dirs, for hero safety,- -- even if that means changing direction.- RunCorridor (if diagonal lxsize d1 then d2 else d1, True)- _ -> RunHub -- a hub of many separate corridors+-- | Continue running in the given direction.+continueRun :: MonadClient m+ => RunParams -> m (Either Msg (RunParams, CmdTakeTimeSer))+continueRun paramOld =+ case paramOld of+ RunParams{ runMembers = []+ , runStopMsg = Just stopMsg } -> return $ Left stopMsg+ RunParams{ runLeader+ , runMembers = r : rs+ , runDist = 0+ , runStopMsg+ , runInitDir = Just dir } ->+ if r == runLeader then do+ -- Start a many-actor run with distance 1, to prevent changing+ -- direction on first turn, if the original direction is blocked.+ -- We want our runners to keep formation.+ let runDistNew = if null rs then 0 else 1+ continueRun paramOld{runDist = runDistNew, runInitDir = Nothing}+ else do+ runOutcome <- continueRunDir r 0 (Just dir)+ case runOutcome of+ Left "" -> do -- hack; means that zeroth step OK+ runStopOrCmd <- moveRunAid r dir+ let runMembersNew = if isJust runStopMsg then rs else rs ++ [r]+ paramNew = paramOld {runMembers = runMembersNew}+ return $! case runStopOrCmd of+ Left stopMsg -> assert `failure` (paramOld, stopMsg)+ Right runCmd -> Right (paramNew, runCmd)+ Left runStopMsgCurrent -> do+ let runStopMsgNew = fromMaybe runStopMsgCurrent runStopMsg+ paramNew = paramOld { runMembers = rs+ , runStopMsg = Just runStopMsgNew }+ continueRun paramNew+ _ -> assert `failure` (paramOld, runOutcome)+ RunParams{ runLeader+ , runMembers = r : rs+ , runDist+ , runStopMsg+ , runInitDir = Nothing } -> do+ let runDistNew = if r == runLeader then runDist + 1 else runDist+ mdirOrRunStopMsgCurrent <- continueRunDir r runDistNew Nothing+ let runStopMsgCurrent =+ either Just (const Nothing) mdirOrRunStopMsgCurrent+ runStopMsgNew = runStopMsg `mplus` runStopMsgCurrent+ -- We check @runStopMsgNew@, because even if the current actor+ -- runs OK, we want to stop soon if some others had to stop.+ runMembersNew = if isJust runStopMsgNew then rs else rs ++ [r]+ paramNew = paramOld { runMembers = runMembersNew+ , runDist = runDistNew+ , runStopMsg = runStopMsgNew }+ case mdirOrRunStopMsgCurrent of+ Left _ -> continueRun paramNew -- run all undisturbed; only one time+ Right dir -> return $ Right (paramNew, MoveSer r dir)+ -- The potential invisible actor is hit. War is started without asking.+ _ -> assert `failure` paramOld --- TODO: express as MonadActionRO--- | Check for disturbances to running such as newly visible items, monsters.-runDisturbance :: Point -> Int -> Report- -> [Actor] -> [Actor] -> Perception -> Bool -> Point- -> (F.Feature -> Point -> Bool) -> (Point -> Bool)- -> Kind.Ops TileKind -> Level -> X -> Y- -> (Vector, Int) -> Maybe (Vector, Int)-runDisturbance posLast distLast report hs ms per markSuspect posHere- posHasFeature posHasItems- cotile lvl lxsize lysize (dirNew, distNew) =- let boringMsgs = map BS.pack [ "You hear some noises." ]- -- TODO: use a regexp from the UI config instead- msgShown = isJust $ findInReport (`notElem` boringMsgs) report- msposs = ES.delete posHere $ ES.fromList (L.map bpos ms)- enemySeen =- not (ES.null (msposs `ES.intersection` totalVisible per))- surrLast = posLast : vicinity lxsize lysize posLast- surrHere = posHere : vicinity lxsize lysize posHere- posThere = posHere `shift` dirNew- heroThere = posThere `elem` L.map bpos hs- -- Stop if you touch any individual tile with these propereties- -- first time, unless you enter it next move, in which case stop then.- touchList = [ posHasFeature F.Exit- , posHasItems- ]- -- Here additionally ignore a tile property if you stand on such tile.- standList = [ posHasFeature F.Path- ]- -- Here stop only if you touch any such tile for the first time.- -- TODO: stop when running along a path and it ends (or turns).- -- TODO: perhaps in open areas change direction to follow lit and paths.- firstList = [ posHasFeature F.Lit- , not . posHasFeature F.Lit- , not . posHasFeature F.Path- , \t -> markSuspect && posHasFeature F.Suspect t- -- TODO: refine for suspect floors (e.g., traps)- ]- -- TODO: stop when walls vanish from cardinal directions or when any- -- walls re-appear again. Actually stop one tile before that happens.- -- Then remove some other, subsumed conditions.- -- This will help with corridors starting in dark rooms.- touchNew fun =- let touchLast = L.filter fun surrLast- touchHere = L.filter fun surrHere- in touchHere L.\\ touchLast- touchExplore fun = touchNew fun == [posThere]- touchStop fun = touchNew fun /= []- standNew fun = L.filter (\pos -> posHasFeature F.Walkable pos ||- Tile.openable cotile (lvl `at` pos))- (touchNew fun)- standExplore fun = not (fun posHere) && standNew fun == [posThere]- standStop fun = not (fun posHere) && standNew fun /= []- firstNew fun = L.all (not . fun) surrLast &&- L.any fun surrHere- firstExplore fun = firstNew fun && fun posThere- firstStop = firstNew- tryRunMaybe- | msgShown || enemySeen- || heroThere || distLast >= 40 = Nothing- | L.any touchExplore touchList = Just (dirNew, 1000)- | L.any standExplore standList = Just (dirNew, 1000)- | L.any firstExplore firstList = Just (dirNew, 1000)- | L.any touchStop touchList = Nothing- | L.any standStop standList = Nothing- | L.any firstStop firstList = Nothing- | otherwise = Just (dirNew, distNew)- in tryRunMaybe+-- | Actor moves or searches or alters. No visible actor at the position.+moveRunAid :: MonadClient m+ => ActorId -> Vector -> m (Either Msg CmdTakeTimeSer)+moveRunAid source dir = do+ cops@Kind.COps{cotile} <- getsState scops+ sb <- getsState $ getActorBody source+ let lid = blid sb+ lvl <- getLevel lid+ let spos = bpos sb -- source position+ tpos = spos `shift` dir -- target position+ t = lvl `at` tpos+ runStopOrCmd =+ -- Movement requires full access.+ if accessible cops lvl spos tpos then+ -- The potential invisible actor is hit. War started without asking.+ Right $ MoveSer source dir+ -- No access, so search and/or alter the tile. Non-walkability is+ -- not implied by the lack of access.+ else if not (Tile.isWalkable cotile t)+ && (isSecretPos lvl tpos -- possible secrets here+ && (Tile.isSuspect cotile t -- not yet searched+ || hideTile cotile lvl tpos /= t) -- searching again+ || Tile.isOpenable cotile t+ || Tile.isClosable cotile t+ || Tile.isChangeable cotile t) then+ if not $ EM.null $ lvl `atI` tpos then+ Left $ showFailureSer AlterBlockItem+ else+ Right $ AlterSer source tpos Nothing+ -- We don't use MoveSer, because we don't hit invisible actors.+ -- The potential invisible actor, e.g., in a wall or in+ -- an inaccessible doorway, is made known, taking a turn.+ -- If server performed an attack for free+ -- on the invisible actor anyway, the player (or AI)+ -- would be tempted to repeatedly hit random walls+ -- in hopes of killing a monster lurking within.+ -- If the action had a cost, misclicks would incur the cost, too.+ -- Right now the player may repeatedly alter tiles trying to learn+ -- about invisible pass-wall actors, but it costs a turn+ -- and does not harm the invisible actors, so it's not tempting.+ -- Ignore a known boring, not accessible tile.+ else Left "never mind"+ return $! runStopOrCmd -- | This function implements the actual logic of running. It checks if we -- have to stop running because something interesting cropped up, -- it ajusts the direction given by the vector if we reached -- a corridor's corner (we never change direction except in corridors) -- and it increments the counter of traversed tiles.-continueRunDir :: MonadClientAbort m- => ActorId -> (Vector, Int)- -> m (Vector, Int)-continueRunDir leader (dirLast, distLast) = do+continueRunDir :: MonadClient m+ => ActorId -> Int -> Maybe Vector -> m (Either Msg Vector)+continueRunDir aid distLast mdir = do+ sreport <- getsClient sreport -- TODO: check the message before it goes into history+ let boringMsgs = map BS.pack [ "You hear some noises."+ , "reveals that the" ]+ boring repLine = any (`BS.isInfixOf` repLine) boringMsgs+ -- TODO: use a regexp from the UI config instead+ msgShown = isJust $ findInReport (not . boring) sreport+ if msgShown then return $ Left "message shown"+ else do+ let maxDistance = 20+ cops@Kind.COps{cotile} <- getsState scops+ body <- getsState $ getActorBody aid+ let lid = blid body+ lvl <- getLevel lid+ let posHere = bpos body+ posLast = boldpos body+ dirLast = displacement posLast posHere+ dir = fromMaybe dirLast mdir+ posThere = posHere `shift` dir+ actorsThere <- getsState $ posToActors posThere lid+ let openableLast = Tile.isOpenable cotile (lvl `at` (posHere `shift` dir))+ check+ | not $ null actorsThere = return $ Left "actor in the way"+ -- don't displace actors, except with leader in step 1+ | distLast >= maxDistance =+ return $ Left $ "reached max run distance" <+> tshow maxDistance+ | accessibleDir cops lvl posHere dir =+ if distLast == 0+ then return $ Left "" -- hack; means that zeroth step OK+ else checkAndRun aid dir+ | distLast /= 1 = return $ Left "blocked"+ -- don't change direction, except in step 1+ | openableLast = return $ Left "blocked by a closed door"+ -- the player may prefer to open the door+ | otherwise =+ -- Assume turning is permitted, because this is the start+ -- of the run, so the situation is mostly known to the player+ tryTurning aid+ check++tryTurning :: MonadClient m+ => ActorId -> m (Either Msg Vector)+tryTurning aid = do cops@Kind.COps{cotile} <- getsState scops- body <- getsState $ getActorBody leader+ body <- getsState $ getActorBody aid let lid = blid body- per <- getPerFid lid- sreport <- getsClient sreport -- TODO: check the message before it goes into history+ lvl <- getLevel lid+ let posHere = bpos body+ posLast = boldpos body+ dirLast = displacement posLast posHere+ let openableDir dir = Tile.isOpenable cotile (lvl `at` (posHere `shift` dir))+ dirEnterable dir = accessibleDir cops lvl posHere dir || openableDir dir+ dirNearby dir1 dir2 = euclidDistSqVector dir1 dir2 `elem` [1, 2]+ dirSimilar dir = dirNearby dirLast dir && dirEnterable dir+ dirsSimilar = filter dirSimilar moves+ case dirsSimilar of+ [] -> return $ Left "dead end"+ d1 : ds | all (dirNearby d1) ds -> -- only one or two directions possible+ case sortBy (compare `on` euclidDistSqVector dirLast)+ $ filter (accessibleDir cops lvl posHere) $ d1 : ds of+ [] ->+ return $ Left "blocked and all similar directions are closed doors"+ d : _ -> checkAndRun aid d+ _ -> return $ Left "blocked and many distant similar directions found"++-- The direction is different than the original, if called from @tryTurning@+-- and the same if from @continueRunDir@.+checkAndRun :: MonadClient m+ => ActorId -> Vector -> m (Either Msg Vector)+checkAndRun aid dir = do+ Kind.COps{cotile=cotile@Kind.Ops{okind}} <- getsState scops+ body <- getsState $ getActorBody aid smarkSuspect <- getsClient smarkSuspect- fact <- getsState $ (EM.! bfid body) . sfactionD- ms <- getsState $ actorList (isAtWar fact) lid- hs <- getsState $ actorList (not . isAtWar fact) lid- lvl@Level{lxsize, lysize} <- getLevel $ blid body+ let lid = blid body+ lvl <- getLevel lid let posHere = bpos body- posHasFeature f pos = Tile.hasFeature cotile f (lvl `at` pos) posHasItems pos = not $ EM.null $ lvl `atI` pos- posLast = if distLast == 0 then posHere else posHere `shift` neg dirLast- tryRunDist (dir, distNew)- | accessibleDir cops lvl posHere dir =- -- TODO: perhaps @abortWith report2?- maybe abort (runDir leader) $- runDisturbance- posLast distLast sreport hs ms per smarkSuspect posHere- posHasFeature posHasItems cotile lvl lxsize lysize (dir, distNew)- | otherwise = abort -- do not open doors in the middle of a run- tryRun dir = tryRunDist (dir, distLast)- _tryRunAndStop dir = tryRunDist (dir, 1000)- openableDir pos dir = Tile.openable cotile (lvl `at` (pos `shift` dir))- dirEnterable pos d = accessibleDir cops lvl pos d || openableDir pos d- case runMode posHere dirLast dirEnterable lxsize of- RunDeadEnd -> abort -- we don't run backwards- RunOpen -> tryRun dirLast -- run forward into the open space- RunHub -> abort -- stop and decide where to go- RunCorridor (dirNext, _turn) -> -- look ahead- tryRun dirNext- -- TODO: instead of a lookahead (does not work, since clients have- -- limited knowledge), pass _turn similarly as in (dir, 1000)- -- and decide next turn.- -- TODO: perhaps boldpos can be handy here- -- case runMode (posHere `shift` dirNext) dirNext dirEnterable lxsize of- -- RunDeadEnd -> tryRun dirNext -- explore the dead end- -- RunCorridor _ -> tryRun dirNext -- follow the corridor- -- RunOpen | turn -> abort -- stop and decide when to turn- -- RunHub | turn -> abort -- stop and decide when to turn- -- RunOpen -> tryRunAndStop dirNext -- no turn, get closer and stop- -- RunHub -> tryRunAndStop dirNext -- no turn, get closer and stop+ posThere = posHere `shift` dir+ actorsThere <- getsState $ posToActors posThere lid+ let posLast = boldpos body+ dirLast = displacement posLast posHere+ -- This is supposed to work on unit vectors --- diagonal, as well as,+ -- vertical and horizontal.+ anglePos :: Point -> Vector -> RadianAngle -> Point+ anglePos pos d angle = shift pos (rotate angle d)+ -- We assume the tiles have not changes since last running step.+ -- If they did, we don't care --- running should be stopped+ -- because of the change of nearby tiles then (TODO).+ -- We don't take into account the two tiles at the rear of last+ -- surroundings, because the actor may have come from there+ -- (via a diagonal move) and if so, he may be interested in such tiles.+ -- If he arrived directly from the right or left, he is responsible+ -- for starting the run further away, if he does not want to ignore+ -- such tiles as the ones he came from.+ tileLast = lvl `at` posLast+ tileHere = lvl `at` posHere+ tileThere = lvl `at` posThere+ leftPsLast = map (anglePos posHere dirLast) [pi/2, 3*pi/4]+ ++ map (anglePos posHere dir) [pi/2, 3*pi/4]+ rightPsLast = map (anglePos posHere dirLast) [-pi/2, -3*pi/4]+ ++ map (anglePos posHere dir) [-pi/2, -3*pi/4]+ leftForwardPosHere = anglePos posHere dir (pi/4)+ rightForwardPosHere = anglePos posHere dir (-pi/4)+ leftTilesLast = map (lvl `at`) leftPsLast+ rightTilesLast = map (lvl `at`) rightPsLast+ leftForwardTileHere = lvl `at` leftForwardPosHere+ rightForwardTileHere = lvl `at` rightForwardPosHere+ featAt = actionFeatures smarkSuspect . okind+ terrainChangeMiddle = null (Tile.causeEffects cotile tileThere)+ -- step into; will stop next turn due to message+ && featAt tileThere+ `notElem` map featAt [tileLast, tileHere]+ terrainChangeLeft = featAt leftForwardTileHere+ `notElem` map featAt leftTilesLast+ terrainChangeRight = featAt rightForwardTileHere+ `notElem` map featAt rightTilesLast+ itemChangeLeft = posHasItems leftForwardPosHere+ `notElem` map posHasItems leftPsLast+ itemChangeRight = posHasItems rightForwardPosHere+ `notElem` map posHasItems rightPsLast+ check+ | not $ null actorsThere = return $ Left "actor in the way"+ -- Actor in possibly another direction tnat original.+ | terrainChangeLeft = return $ Left "terrain change on the left"+ | terrainChangeRight = return $ Left "terrain change on the right"+ | itemChangeLeft = return $ Left "item change on the left"+ | itemChangeRight = return $ Left "item change on the right"+ | terrainChangeMiddle = return $ Left "terrain change in the middle"+ | otherwise = return $ Right dir+ check
Game/LambdaHack/Client/State.hs view
@@ -1,16 +1,19 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Server and client game state types and operations. module Game.LambdaHack.Client.State ( StateClient(..), defStateClient, defHistory , updateTarget, getTarget, updateLeader, sside- , TgtMode(..), Target(..)+ , PathEtc, TgtMode(..), Target(..), RunParams(..), LastRecord , toggleMarkVision, toggleMarkSmell, toggleMarkSuspect ) where +import Control.Exception.Assert.Sugar import Control.Monad import Data.Binary import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.Text (Text) import qualified Data.Text as T-import Game.LambdaHack.Common.Vector import qualified NLP.Miniutter.English as MU import qualified System.Random as R import System.Time@@ -27,8 +30,10 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point+import qualified Game.LambdaHack.Common.PointArray as PointArray+import Game.LambdaHack.Common.ServerCmd import Game.LambdaHack.Common.State-import Control.Exception.Assert.Sugar+import Game.LambdaHack.Common.Vector -- | Client state, belonging to a single faction. -- Some of the data, e.g, the history, carries over@@ -36,54 +41,93 @@ -- Data invariant: if @_sleader@ is @Nothing@ then so is @srunning@. data StateClient = StateClient { stgtMode :: !(Maybe TgtMode) -- ^ targeting mode- , scursor :: !(Maybe Point) -- ^ cursor coordinates+ , scursor :: !Target -- ^ the common, cursor target , seps :: !Int -- ^ a parameter of the tgt digital line- , stargetD :: !(EM.EnumMap ActorId Target)+ , stargetD :: !(EM.EnumMap ActorId (Target, Maybe PathEtc)) -- ^ targets of our actors in the dungeon- , srunning :: !(Maybe (Vector, Int))- -- ^ direction and distance of running+ , sexplored :: !(ES.EnumSet LevelId)+ -- ^ the set of fully explored levels+ , sbfsD :: !(EM.EnumMap ActorId+ ( PointArray.Array BfsDistance+ , Point, Int, Maybe [Point]) )+ -- ^ pathfinding distances for our actors+ -- and paths to their targets, if any+ , sselected :: !(ES.EnumSet ActorId)+ -- ^ the set of currently selected actors+ , srunning :: !(Maybe RunParams)+ -- ^ parameters of the current run, if any , sreport :: !Report -- ^ current messages , shistory :: !History -- ^ history of messages , sundo :: ![Atomic] -- ^ atomic commands performed to date , sdisco :: !Discovery -- ^ remembered item discoveries , sfper :: !FactionPers -- ^ faction perception indexed by levels , srandom :: !R.StdGen -- ^ current random generator- , sconfigUI :: !ConfigUI -- ^ client config (including initial RNG)+ , sconfigUI :: ConfigUI -- ^ client config (including initial RNG) , slastKey :: !(Maybe K.KM) -- ^ last command key pressed- , _sleader :: !(Maybe ActorId) -- ^ selected actor+ , slastRecord :: !LastRecord -- ^ state of key sequence recording+ , slastPlay :: ![K.KM] -- ^ state of key sequence playback+ , slastCmd :: !(Maybe CmdTakeTimeSer)+ -- ^ last command sent to the server+ , swaitTimes :: !Int -- ^ player just waited this many times+ , _sleader :: !(Maybe ActorId)+ -- ^ current picked party leader , _sside :: !FactionId -- ^ faction controlled by the client , squit :: !Bool -- ^ exit the game loop , sisAI :: !Bool -- ^ whether it's an AI client , smarkVision :: !Bool -- ^ mark leader and party FOV , smarkSmell :: !Bool -- ^ mark smell, if the leader can smell , smarkSuspect :: !Bool -- ^ mark suspect features+ , scurDifficulty :: !Int -- ^ current game difficulty level , sdebugCli :: !DebugModeCli -- ^ client debugging mode }- deriving (Show)+ deriving Show +type PathEtc = ([Point], (Point, Int))+ -- | Current targeting mode of a client.-data TgtMode =- TgtExplicit { tgtLevelId :: !LevelId }- -- ^ the player requested targeting mode explicitly- | TgtAuto { tgtLevelId :: !LevelId }- -- ^ the mode was entered (and will be exited) automatically- deriving (Show, Eq)+newtype TgtMode = TgtMode { tgtLevelId :: LevelId }+ deriving (Show, Eq, Binary) -- | The type of na actor target. data Target =- TEnemy !ActorId !Point -- ^ target an actor with its last seen position- | TPos !Point -- ^ target a given position+ TEnemy !ActorId !Bool+ -- ^ target an actor; cycle only trough seen foes, unless the flag is set+ | TEnemyPos !ActorId !LevelId !Point !Bool+ -- ^ last seen position of the targeted actor+ | TPoint !LevelId !Point -- ^ target a concrete spot+ | TVector !Vector -- ^ target position relative to actor deriving (Show, Eq) +-- | Parameters of the current run.+data RunParams = RunParams+ { runLeader :: !ActorId -- ^ the original leader from run start+ , runMembers :: ![ActorId] -- ^ the list of actors that take part+ , runDist :: !Int -- ^ distance of the run so far+ -- (plus one, if multiple runners)+ , runStopMsg :: !(Maybe Text) -- ^ message with the next stop reason+ , runInitDir :: !(Maybe Vector) -- ^ the direction of the initial step+ }+ deriving (Show)++type LastRecord = ( [K.KM] -- accumulated keys of the current command+ , [K.KM] -- keys of the rest of the recorded command batch+ , Int -- commands left to record for this batch+ )+ -- | Initial game client state. defStateClient :: History -> ConfigUI -> FactionId -> Bool -> StateClient defStateClient shistory sconfigUI _sside sisAI = StateClient { stgtMode = Nothing- , scursor = Nothing+ , scursor = if sisAI+ then TVector $ Vector 30000 30000 -- invalid+ else TVector $ Vector 1 1 -- a step south-east , seps = 0 , stargetD = EM.empty+ , sexplored = ES.empty+ , sbfsD = EM.empty+ , sselected = ES.empty , srunning = Nothing , sreport = emptyReport , shistory@@ -93,6 +137,10 @@ , sconfigUI , srandom = R.mkStdGen 42 -- will be set later , slastKey = Nothing+ , slastRecord = ([], [], 0)+ , slastPlay = []+ , slastCmd = Nothing+ , swaitTimes = 0 , _sleader = Nothing -- no heroes yet alive , _sside , squit = False@@ -100,6 +148,7 @@ , smarkVision = False , smarkSmell = False , smarkSuspect = False+ , scurDifficulty = 0 , sdebugCli = defDebugModeCli } @@ -107,19 +156,23 @@ defHistory = do dateTime <- getClockTime let curDate = MU.Text $ T.pack $ calendarTimeToString $ toUTCTime dateTime- return $ singletonHistory $ singletonReport- $ makeSentence ["Human history log started on", curDate]+ return $! singletonHistory $ singletonReport+ $! makeSentence ["Human history log started on", curDate] -- | Update target parameters within client state. updateTarget :: ActorId -> (Maybe Target -> Maybe Target) -> StateClient -> StateClient-updateTarget aid f cli = cli { stargetD = EM.alter f aid (stargetD cli) }+updateTarget aid f cli =+ let f2 tp = case f $ fmap fst tp of+ Nothing -> Nothing+ Just tgt -> Just (tgt, Nothing) -- reset path+ in cli {stargetD = EM.alter f2 aid (stargetD cli)} -- | Get target parameters from client state. getTarget :: ActorId -> StateClient -> Maybe Target-getTarget aid cli = EM.lookup aid (stargetD cli)+getTarget aid cli = fmap fst $ EM.lookup aid $ stargetD cli --- | Update selected actor within state. Verify actor's faction.+-- | Update picked leader within state. Verify actor's faction. updateLeader :: ActorId -> State -> StateClient -> StateClient updateLeader leader s cli = let side1 = bfid $ getActorBody leader s@@ -147,61 +200,81 @@ put scursor put seps put stargetD+ put sexplored+ put sselected put srunning put sreport put shistory put sundo put sdisco put (show srandom)- put sconfigUI put _sleader put _sside put sisAI put smarkVision put smarkSmell put smarkSuspect- put sdebugCli+ put scurDifficulty+ put sdebugCli -- TODO: this is overwritten at once get = do stgtMode <- get scursor <- get seps <- get stargetD <- get+ sexplored <- get+ sselected <- get srunning <- get sreport <- get shistory <- get sundo <- get sdisco <- get g <- get- sconfigUI <- get _sleader <- get _sside <- get sisAI <- get smarkVision <- get smarkSmell <- get smarkSuspect <- get+ scurDifficulty <- get sdebugCli <- get- let sfper = EM.empty+ let sbfsD = EM.empty+ sfper = EM.empty srandom = read g slastKey = Nothing+ slastRecord = ([], [], 0)+ slastPlay = []+ slastCmd = Nothing+ swaitTimes = 0 squit = False- return StateClient{..}+ sconfigUI = undefined+ return $! StateClient{..} -instance Binary TgtMode where- put (TgtExplicit l) = putWord8 0 >> put l- put (TgtAuto l) = putWord8 1 >> put l+instance Binary RunParams where+ put RunParams{..} = do+ put runLeader+ put runMembers+ put runDist+ put runStopMsg+ put runInitDir get = do- tag <- getWord8- case tag of- 0 -> liftM TgtExplicit get- 1 -> liftM TgtAuto get- _ -> fail "no parse (TgtMode)"+ runLeader <- get+ runMembers <- get+ runDist<- get+ runStopMsg <- get+ runInitDir <- get+ return $! RunParams{..} instance Binary Target where- put (TEnemy a ll) = putWord8 0 >> put a >> put ll- put (TPos pos) = putWord8 1 >> put pos+ put (TEnemy a permit) = putWord8 0 >> put a >> put permit+ put (TEnemyPos a lid p permit) =+ putWord8 1 >> put a >> put lid >> put p >> put permit+ put (TPoint lid p) = putWord8 2 >> put lid >> put p+ put (TVector v) = putWord8 3 >> put v get = do tag <- getWord8 case tag of 0 -> liftM2 TEnemy get get- 1 -> liftM TPos get+ 1 -> liftM4 TEnemyPos get get get get+ 2 -> liftM2 TPoint get get+ 3 -> liftM TVector get _ -> fail "no parse (Target)"
Game/LambdaHack/Client/Strategy.hs view
@@ -22,7 +22,8 @@ -- | Strategy is a monad. TODO: Can we write this as a monad transformer? instance Monad Strategy where- return x = Strategy $ return $ uniformFreq "Strategy_return" [x]+ {-# INLINE return #-}+ return x = Strategy $ return $! uniformFreq "Strategy_return" [x] m >>= f = normalizeStrategy $ Strategy [ toFreq name [ (p * q, b) | (p, a) <- runFrequency x@@ -36,16 +37,17 @@ fmap f (Strategy fs) = Strategy (map (fmap f) fs) instance Applicative Strategy where- pure = return- (<*>) = ap+ pure = return+ (<*>) = ap instance MonadPlus Strategy where mzero = Strategy []+ {-# INLINE mplus #-} mplus (Strategy xs) (Strategy ys) = Strategy (xs ++ ys) instance Alternative Strategy where- (<|>) = mplus- empty = mzero+ (<|>) = mplus+ empty = mzero normalizeStrategy :: Strategy a -> Strategy a normalizeStrategy (Strategy fs) = Strategy $ filter (not . nullFreq) fs@@ -95,4 +97,4 @@ -- | Like 'return', but pick a name of the single frequency. returN :: Text -> a -> Strategy a-returN name x = Strategy $ return $ uniformFreq name [x]+returN name x = Strategy $ return $! uniformFreq name [x]
Game/LambdaHack/Client/StrategyAction.hs view
@@ -1,13 +1,16 @@ -- | AI strategy operations implemented with the 'Action' monad. module Game.LambdaHack.Client.StrategyAction- ( targetStrategy, actionStrategy, visibleFoes+ ( targetStrategy, actionStrategy ) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES import Data.Function import Data.List import Data.Maybe+import Data.Ord import qualified Data.Traversable as Traversable import Game.LambdaHack.Client.Action@@ -24,8 +27,8 @@ import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level-import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.Random import qualified Game.LambdaHack.Common.Random as Random import Game.LambdaHack.Common.ServerCmd import Game.LambdaHack.Common.State@@ -33,184 +36,294 @@ import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind as TileKind-import Control.Exception.Assert.Sugar import Game.LambdaHack.Utils.Frequency -- | AI proposes possible targets for the actor. Never empty.-targetStrategy :: MonadClient m- => ActorId -> [Ability] -> m (Strategy (Maybe Target))-targetStrategy aid factionAbilities = do- btarget <- getsClient $ getTarget aid- fper <- getsClient sfper- reacquireTgt aid factionAbilities btarget fper--visibleFoes :: MonadActionRO m- => ActorId -> FactionPers -> m [(ActorId, Actor)]-visibleFoes aid fper = do+targetStrategy :: forall m. MonadClient m+ => ActorId -> ActorId -> m (Strategy (Target, PathEtc))+targetStrategy oldLeader aid = do+ Kind.COps{ cotile=cotile@Kind.Ops{ouniqGroup}+ , coactor=Kind.Ops{okind}+ , cofaction=Kind.Ops{okind=fokind} } <- getsState scops+ modifyClient $ \cli -> cli {sbfsD = EM.delete aid (sbfsD cli)} b <- getsState $ getActorBody aid+ mtgtMPath <- getsClient $ EM.lookup aid . stargetD+ oldTgtUpdatedPath <- case mtgtMPath of+ Just (tgt, Just path) -> do+ mvalidPos <- aidTgtToPos aid (blid b) (Just tgt)+ if isNothing mvalidPos then return Nothing -- wrong level+ else return $! case path of+ (p : q : rest, (goal, len)) ->+ if bpos b == p+ then Just (tgt, path) -- no move last turn+ else if bpos b == q+ then Just (tgt, (q : rest, (goal, len - 1))) -- step along path+ else Nothing -- veered off the path+ ([p], (goal, _)) -> do+ assert (p == goal `blame` (aid, b, mtgtMPath)) skip+ if bpos b == p then+ Just (tgt, path) -- goal reached; stay there picking up items+ else+ Nothing -- somebody pushed us off the goal; let's target again+ ([], _) -> assert `failure` (aid, b, mtgtMPath)+ Just (_, Nothing) -> return Nothing -- path invalidated, e.g. SpotActorA+ Nothing -> return Nothing -- no target assigned yet+ lvl <- getLevel $ blid b assert (not $ bproj b) skip -- would work, but is probably a bug- let per = fper EM.! blid b fact <- getsState $ \s -> sfactionD s EM.! bfid b- foes <- getsState $ actorNotProjAssocs (isAtWar fact) (blid b)- return $! filter (actorSeesPos per aid . bpos . snd) foes--reacquireTgt :: MonadActionRO m- => ActorId -> [Ability] -> Maybe Target -> FactionPers- -> m (Strategy (Maybe Target))-reacquireTgt aid factionAbilities btarget fper = do- cops@Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops- b <- getsState $ getActorBody aid- assert (not $ bproj b) skip -- would work, but is probably a bug- lvl@Level{lxsize} <- getsState $ \s -> sdungeon s EM.! blid b- visFoes <- visibleFoes aid fper- actorD <- getsState sactorD- -- TODO: set distant targets so that monsters behave as if they have- -- a plan. We need pathfinding for that.- noFoes :: Strategy (Maybe Target) <- do- s <- getState- str <- moveStrategy cops aid s Nothing- return $ (Just . TPos . (bpos b `shift`)) `liftM` str- let per = fper EM.! blid b- mk = okind $ bkind b- actorAbilities = acanDo mk `intersect` factionAbilities- focused = bspeed b <= speedNormal- -- Don't focus on a distant enemy, when you can't chase him.- -- TODO: or only if another enemy adjacent? consider Flee?- && Ability.Chase `elem` actorAbilities- closest :: Strategy (Maybe Target)- closest =- let distB = chessDist lxsize (bpos b)- foeDist = map (\(_, body) -> distB (bpos body)) visFoes- minDist | null foeDist = maxBound- | otherwise = minimum foeDist- minFoes =- filter (\(_, body) -> distB (bpos body) == minDist) visFoes- minTargets = map (\(a, body) ->- Just $ TEnemy a $ bpos body) minFoes- minTgtS = liftFrequency $ uniformFreq "closest" minTargets- in minTgtS .| noFoes .| returN "TCursor" Nothing -- never empty- reacquire :: Maybe Target -> Strategy (Maybe Target)- reacquire tgt =- case tgt of- Just (TEnemy a ll) | focused -> -- chase even if enemy dead, to loot- case fmap bpos $ EM.lookup a actorD of- Just l | actorSeesPos per aid l ->- -- prefer visible (and alive) foes- returN "TEnemy" $ Just $ TEnemy a l- _ -> if null visFoes -- prefer visible foes to positions- && bpos b /= ll -- not yet reached the last pos- then returN "last known" $ Just $ TPos ll- -- chase the last known pos- else closest- Just TEnemy{} -> closest -- just pick the closest foe- Just (TPos pos) | bpos b == pos -> closest -- already reached pos- Just (TPos pos)- | not $ bumpableHere cops lvl False (asight mk) pos ->- closest -- no longer bumpable, even assuming no foes- Just TPos{} | null visFoes -> returN "TPos" tgt- -- nothing visible, go to pos- Just TPos{} -> closest -- prefer visible foes- Nothing -> closest- return $! reacquire btarget+ allFoes <- getsState $ actorNotProjAssocs (isAtWar fact) (blid b)+ dungeon <- getsState sdungeon+ -- TODO: we assume the actor eventually becomes a leader (or has the same+ -- set of abilities as the leader, anyway) and set his target accordingly.+ actorAbs <- actorAbilities aid (Just aid)+ let nearby = 10+ nearbyFoes = filter (\(_, body) ->+ chessDist (bpos body) (bpos b) < nearby) allFoes+ unknownId = ouniqGroup "unknown space"+ -- TODO: make more common when weak ranged foes preferred, etc.+ focused = bspeed b < speedNormal+ canSmell = asmell $ okind $ bkind b+ setPath :: Target -> m (Strategy (Target, PathEtc))+ setPath tgt = do+ mpos <- aidTgtToPos aid (blid b) (Just tgt)+ let p = fromMaybe (assert `failure` (b, tgt)) mpos+ (bfs, mpath) <- getCacheBfsAndPath aid p+ case mpath of+ Nothing -> assert `failure` "new target unreachable" `twith` (b, tgt)+ Just path ->+ return $! returN "pickNewTarget"+ (tgt, ( bpos b : path+ , (p, fromMaybe (assert `failure` mpath)+ $ accessBfs bfs p) ))+ pickNewTarget :: m (Strategy (Target, PathEtc))+ pickNewTarget = do+ -- TODO: for foes, items, etc. consider a few nearby, not just one+ cfoes <- closestFoes aid+ case cfoes of+ (_, (a, _)) : _ -> setPath $ TEnemy a False+ [] -> do+ -- Tracking enemies is more important than exploring,+ -- and smelling actors are usually blind, so bad at exploring.+ -- TODO: prefer closer items to older smells+ smpos <- if canSmell+ then closestSmell aid+ else return []+ case smpos of+ [] -> do+ citems <- if Ability.Pickup `elem` actorAbs+ then closestItems aid+ else return []+ case citems of+ [] -> do+ upos <- closestUnknown aid+ case upos of+ Nothing -> do+ ctriggers <- if Ability.Trigger `elem` actorAbs+ then closestTriggers Nothing False aid+ else return []+ case ctriggers of+ [] -> do+ getDistant <-+ rndToAction $ oneOf+ $ [fmap maybeToList . furthestKnown]+ ++ [ closestTriggers Nothing True+ | EM.size dungeon > 1 ]+ kpos <- getDistant aid+ case kpos of+ [] -> return reject+ p : _ -> setPath $ TPoint (blid b) p+ p : _ -> setPath $ TPoint (blid b) p+ Just p -> setPath $ TPoint (blid b) p+ (_, (p, _)) : _ -> setPath $ TPoint (blid b) p+ (_, (p, _)) : _ -> setPath $ TPoint (blid b) p+ tellOthersNothingHere pos = do+ let f (tgt, _) = case tgt of+ TEnemyPos _ lid p _ -> p /= pos || lid /= blid b+ _ -> True+ modifyClient $ \cli -> cli {stargetD = EM.filter f (stargetD cli)}+ pickNewTarget+ updateTgt :: Target -> PathEtc+ -> m (Strategy (Target, PathEtc))+ updateTgt oldTgt updatedPath = case oldTgt of+ TEnemy a _ -> do+ body <- getsState $ getActorBody a+ if not focused -- prefers closer foes+ && not (null nearbyFoes) -- foes nearby+ && a `notElem` map fst nearbyFoes -- old one not close enough+ || blid body /= blid b -- wrong level+ then pickNewTarget+ else if bpos body == fst (snd updatedPath)+ then return $! returN "TEnemy" (oldTgt, updatedPath)+ -- The enemy didn't move since the target acquired.+ -- If any walls were added that make the enemy+ -- unreachable, AI learns that the hard way,+ -- as soon as it bumps into them.+ else do+ let p = bpos body+ (bfs, mpath) <- getCacheBfsAndPath aid p+ case mpath of+ Nothing -> pickNewTarget -- enemy became unreachable+ Just path ->+ return $! returN "TEnemy"+ (oldTgt, ( bpos b : path+ , (p, fromMaybe (assert `failure` mpath)+ $ accessBfs bfs p) ))+ _ | not $ null nearbyFoes ->+ pickNewTarget -- prefer close foes to anything+ TPoint lid pos -> do+ explored <- getsClient sexplored+ let allExplored = ES.size explored == EM.size dungeon+ abilityLeader = fAbilityLeader $ fokind $ gkind fact+ abilityOther = fAbilityOther $ fokind $ gkind fact+ if lid /= blid b -- wrong level+ -- Below we check the target could not be picked again in+ -- pickNewTarget, and only in this case it is invalidated.+ -- This ensures targets are eventually reached (unless a foe+ -- shows up) and not changed all the time mid-route+ -- to equally interesting, but perhaps a bit closer targets,+ -- most probably already targeted by other actors.+ || (Ability.Pickup `notElem` actorAbs -- closestItems+ || EM.null (lvl `atI` pos))+ && (not canSmell -- closestSmell+ || pos == bpos b -- in case server resends deleted smell+ || let sml =+ EM.findWithDefault timeZero pos (lsmell lvl)+ in sml `timeAdd` timeNegate (ltime lvl) <= timeZero)+ && let t = lvl `at` pos+ in if ES.notMember lid explored+ then -- closestUnknown+ t /= unknownId+ && not (Tile.isSuspect cotile t)+ else -- closestTriggers+ -- Try to kill that very last enemy for his loot before+ -- leaving the level or dungeon.+ not (null allFoes)+ || -- If all explored, escape/block escapes.+ (Ability.Trigger `notElem` actorAbs+ || not (Tile.isEscape cotile t && allExplored))+ -- The next case is stairs in closestTriggers.+ -- We don't determine if the stairs are interesting+ -- (this changes with time), but allow the actor+ -- to reach them and then retarget.+ && not (pos /= bpos b && Tile.isStair cotile t)+ -- The remaining case is furthestKnown. This is+ -- always an unimportant target, so we forget it+ -- if the actor is stuck (could move, but waits).+ && let isStuck =+ waitedLastTurn b+ && (oldLeader == aid+ || abilityLeader == abilityOther)+ in not (pos /= bpos b+ && not isStuck+ && allExplored)+ then pickNewTarget+ else return $! returN "TPoint" (oldTgt, updatedPath)+ _ | not $ null allFoes ->+ pickNewTarget -- new likely foes location spotted, forget the old+ TEnemyPos _ lid p _ ->+ -- Chase last position even if foe hides or dies,+ -- to find his companions, loot, etc.+ if lid /= blid b -- wrong level+ then pickNewTarget+ else if p == bpos b+ then tellOthersNothingHere p+ else return $! returN "TEnemyPos" (oldTgt, updatedPath)+ TVector{} -> pickNewTarget+ case oldTgtUpdatedPath of+ Just (oldTgt, updatedPath) -> updateTgt oldTgt updatedPath+ Nothing -> pickNewTarget --- | AI strategy based on actor's sight, smell, intelligence, etc. Never empty.-actionStrategy :: MonadClient m- => ActorId -> [Ability] -> m (Strategy CmdSerTakeTime)-actionStrategy aid factionAbilities = do+-- | AI strategy based on actor's sight, smell, intelligence, etc.+-- Never empty.+actionStrategy :: forall m. MonadClient m+ => ActorId -> m (Strategy CmdTakeTimeSer)+actionStrategy aid = do+ cops <- getsState scops disco <- getsClient sdisco btarget <- getsClient $ getTarget aid- proposeAction disco aid factionAbilities btarget--proposeAction :: MonadActionRO m- => Discovery -> ActorId -> [Ability] -> Maybe Target- -> m (Strategy CmdSerTakeTime)-proposeAction disco aid factionAbilities btarget = do- Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops- Actor{bkind, bpos, blid} <- getsState $ getActorBody aid+ Actor{bpos, blid} <- getsState $ getActorBody aid+ bitems <- getsState $ getActorItem aid+ lootItems <- getsState $ getFloorItem blid bpos lvl <- getLevel blid- let mk = okind bkind- (fpos, mfAid) =+ mleader <- getsClient _sleader+ actorAbs <- actorAbilities aid mleader+ let mfAid = case btarget of- Just (TEnemy foeAid l) -> (l, Just foeAid)- Just (TPos l) -> (l, Nothing)- Nothing -> (bpos, Nothing) -- an actor blocked by friends or a missile+ Just (TEnemy foeAid _) -> Just foeAid+ _ -> Nothing foeVisible = isJust mfAid lootHere x = not $ EM.null $ lvl `atI` x- actorAbilities = acanDo mk `intersect` factionAbilities+ lootIsWeapon = isJust $ strongestSword cops lootItems+ hasNoWeapon = isNothing $ strongestSword cops bitems isDistant = (`elem` [ Ability.Trigger , Ability.Ranged , Ability.Tools , Ability.Chase ]) -- TODO: this is too fragile --- depends on order of abilities- (prefix, rest) = break isDistant actorAbilities+ (prefix, rest) = break isDistant actorAbs (distant, suffix) = partition isDistant rest- -- TODO: Ranged and Tools should only be triggered in some situations.- aFrequency :: MonadActionRO m => Ability -> m (Frequency CmdSerTakeTime)+ aFrequency :: Ability -> m (Frequency CmdTakeTimeSer) aFrequency Ability.Trigger = if foeVisible then return mzero else triggerFreq aid- aFrequency Ability.Ranged = if not foeVisible then return mzero- else rangedFreq disco aid fpos- aFrequency Ability.Tools = if not foeVisible then return mzero- else toolsFreq disco aid- aFrequency Ability.Chase = if fpos == bpos then return mzero- else chaseFreq- aFrequency ab = assert `failure` "unexpected ability"- `twith` (ab, distant, actorAbilities)- chaseFreq :: MonadActionRO m => m (Frequency CmdSerTakeTime)+ aFrequency Ability.Ranged = rangedFreq aid+ aFrequency Ability.Tools = if not foeVisible then return mzero+ else toolsFreq disco aid+ aFrequency Ability.Chase = if not foeVisible then return mzero+ else chaseFreq+ aFrequency ab = assert `failure` "unexpected ability"+ `twith` (ab, distant, actorAbs)+ chaseFreq :: MonadActionRO m => m (Frequency CmdTakeTimeSer) chaseFreq = do- st <- chase aid (fpos, foeVisible)- return $ scaleFreq 30 $ bestVariant st- aStrategy :: MonadActionRO m => Ability -> m (Strategy CmdSerTakeTime)+ st <- chase aid True+ return $! scaleFreq 30 $ bestVariant st+ aStrategy :: Ability -> m (Strategy CmdTakeTimeSer) aStrategy Ability.Track = track aid- aStrategy Ability.Heal = return mzero -- TODO- aStrategy Ability.Flee = return mzero -- TODO- aStrategy Ability.Melee | Just foeAid <- mfAid = melee aid fpos foeAid- aStrategy Ability.Melee = return mzero- aStrategy Ability.Pickup | not foeVisible && lootHere bpos = pickup aid- aStrategy Ability.Pickup = return mzero- aStrategy Ability.Wander = wander aid+ aStrategy Ability.Heal = return reject -- TODO+ aStrategy Ability.Flee = return reject -- TODO+ aStrategy Ability.Melee | foeVisible = melee aid+ aStrategy Ability.Melee = return reject+ aStrategy Ability.Displace = displace aid+ aStrategy Ability.Pickup | not foeVisible && lootHere bpos+ || hasNoWeapon && lootIsWeapon = pickup aid+ aStrategy Ability.Pickup = return reject+ aStrategy Ability.Wander = chase aid False aStrategy ab = assert `failure` "unexpected ability"- `twith`(ab, actorAbilities)+ `twith`(ab, actorAbs) sumS abis = do fs <- mapM aStrategy abis- return $ msum fs+ return $! msum fs sumF abis = do fs <- mapM aFrequency abis- return $ msum fs+ return $! msum fs combineDistant as = fmap liftFrequency $ sumF as sumPrefix <- sumS prefix comDistant <- combineDistant distant sumSuffix <- sumS suffix- return $ sumPrefix .| comDistant .| sumSuffix- -- Wait until friends sidestep; ensures the strategy is never empty.- -- TODO: try to switch leader away before that (we already- -- switch him afterwards)- .| waitBlockNow aid+ return $! sumPrefix .| comDistant .| sumSuffix+ -- Wait until friends sidestep; ensures strategy is never empty.+ -- TODO: try to switch leader away before that (we already+ -- switch him afterwards)+ .| waitBlockNow aid -- | A strategy to always just wait.-waitBlockNow :: ActorId -> Strategy CmdSerTakeTime+waitBlockNow :: ActorId -> Strategy CmdTakeTimeSer waitBlockNow aid = returN "wait" $ WaitSer aid --- | Strategy for dumb missiles.-track :: MonadActionRO m => ActorId -> m (Strategy CmdSerTakeTime)+-- | Strategy for a dumb missile or a strongly hurled actor.+track :: MonadActionRO m => ActorId -> m (Strategy CmdTakeTimeSer) track aid = do- cops <- getsState scops- b@Actor{bpos, bpath, blid} <- getsState $ getActorBody aid- lvl <- getLevel blid- let clearPath = returN "ClearPathSer" $ SetPathSer aid []- strat = case bpath of- Nothing -> reject- Just [] -> assert `failure` "null path" `twith` (aid, b)- -- TODO: instead let server do this in MoveSer, abort, handle in loop- Just (d : _) | not $ accessibleDir cops lvl bpos d -> clearPath- Just lv -> returN "SetPathSer" $ SetPathSer aid lv- return strat+ btrajectory <- getsState $ btrajectory . getActorBody aid+ return $! if isNothing btrajectory+ then reject+ else returN "SetTrajectorySer" $ SetTrajectorySer aid -- TODO: (most?) animals don't pick up. Everybody else does.-pickup :: MonadActionRO m => ActorId -> m (Strategy CmdSerTakeTime)+-- TODO: pick up best weapons first+pickup :: MonadActionRO m => ActorId -> m (Strategy CmdTakeTimeSer) pickup aid = do body@Actor{bpos, blid} <- getsState $ getActorBody aid lvl <- getLevel blid@@ -219,107 +332,191 @@ Just ((iid, k), _) -> do -- pick up first item item <- getsState $ getItemBody iid let l = if jsymbol item == '$' then Just $ InvChar '$' else Nothing- return $ case assignLetter iid l body of- Just l2 -> returN "pickup" $ PickupSer aid iid k l2+ return $! case assignLetter iid l body of+ Just _ -> returN "pickup" $ PickupSer aid iid k Nothing -> returN "pickup" $ WaitSer aid -- TODO- return actionPickup+ return $! actionPickup -- Everybody melees in a pinch, even though some prefer ranged attacks.-melee :: MonadActionRO m- => ActorId -> Point -> ActorId -> m (Strategy CmdSerTakeTime)-melee aid fpos foeAid = do- Actor{bpos, blid} <- getsState $ getActorBody aid- Level{lxsize} <- getLevel blid- let foeAdjacent = adjacent lxsize bpos fpos -- MeleeDistant- return $ foeAdjacent .=> returN "melee" (MeleeSer aid foeAid)+melee :: MonadClient m => ActorId -> m (Strategy CmdTakeTimeSer)+melee aid = do+ b <- getsState $ getActorBody aid+ fact <- getsState $ \s -> sfactionD s EM.! bfid b+ mtgtMPath <- getsClient $ EM.lookup aid . stargetD+ str1 <- case mtgtMPath of+ Just (_, Just (_ : q : _, (goal, _))) -> do+ -- We prefer the goal (e.g., when no accessible, but adjacent),+ -- but accept @q@ even if it's only a blocking enemy position.+ let maim = if adjacent (bpos b) goal then Just goal+ else if adjacent (bpos b) q then Just q+ else Nothing -- MeleeDistant+ mBlocker <- case maim of+ Nothing -> return Nothing+ Just aim -> getsState $ posToActor aim (blid b)+ case mBlocker of+ Just ((aid2, _), _) -> do+ -- No problem if there are many projectiles at the spot. We just+ -- attack the first one.+ body2 <- getsState $ getActorBody aid2+ if isAtWar fact (bfid body2) then+ return $! returN "melee in the way" (MeleeSer aid aid2)+ else return reject+ Nothing -> return reject+ _ -> return reject -- probably no path to the foe, if any+ -- TODO: depending on actor kind, sometimes move this in strategy+ -- to a place after movement+ if not $ nullStrategy str1 then return str1 else do+ Level{lxsize, lysize} <- getLevel $ blid b+ allFoes <- getsState $ actorNotProjAssocs (isAtWar fact) (blid b)+ let vic = vicinity lxsize lysize $ bpos b+ adjFoes = filter ((`elem` vic) . bpos . snd) allFoes+ -- TODO: prioritize somehow+ freq = uniformFreq "melee adjacent" $ map (MeleeSer aid . fst) adjFoes+ return $ liftFrequency freq -- Fast monsters don't pay enough attention to features.-triggerFreq :: MonadActionRO m- => ActorId -> m (Frequency CmdSerTakeTime)+triggerFreq :: MonadClient m => ActorId -> m (Frequency CmdTakeTimeSer) triggerFreq aid = do cops@Kind.COps{cotile=Kind.Ops{okind}} <- getsState scops- b@Actor{bpos, blid, bfid, boldpos} <- getsState $ getActorBody aid- fact <- getsState $ \s -> sfactionD s EM.! bfid- lvl <- getLevel blid- let spawn = isSpawnFact cops fact- t = lvl `at` bpos+ dungeon <- getsState sdungeon+ explored <- getsClient sexplored+ b <- getsState $ getActorBody aid+ fact <- getsState $ \s -> sfactionD s EM.! bfid b+ lvl <- getLevel $ blid b+ unexploredD <- unexploredDepth+ s <- getState+ let unexploredCurrent = ES.notMember (blid b) explored+ allExplored = ES.size explored == EM.size dungeon+ isHero = isHeroFact cops fact+ t = lvl `at` bpos b feats = TileKind.tfeature $ okind t ben feat = case feat of- F.Cause Effect.Escape | spawn -> 0 -- spawners lose if they escape+ F.Cause (Effect.Ascend k) -> -- change levels sensibly, in teams+ let expBenefit =+ if unexploredCurrent+ then 0 -- don't leave the level until explored+ else if unexploredD (signum k) (blid b)+ then 1000+ else if unexploredD (- signum k) (blid b)+ then 0 -- wait for stairs in the opposite direciton+ else if lescape lvl+ then 0 -- all explored, stay on the escape level+ else 2 -- no escape anywhere, switch levels occasionally+ (lid2, pos2) = whereTo (blid b) (bpos b) k dungeon+ actorsThere = posToActors pos2 lid2 s+ in if boldpos b == bpos b -- probably used stairs last turn+ && boldlid b == lid2 -- in the opposite direction+ then 0 -- avoid trivial loops (pushing, being pushed, etc.)+ else case actorsThere of+ [] -> expBenefit+ [((_, body), _)] | not (bproj body)+ && isAtWar fact (bfid body) ->+ min 1 expBenefit -- push the enemy if no better option+ _ -> 0 -- projectiles or non-enemies+ F.Cause ef@Effect.Escape{} ->+ -- Only heroes escape but they first explore all for high score.+ if not (isHero && allExplored) then 0 else effectToBenefit cops b ef F.Cause ef -> effectToBenefit cops b ef _ -> 0 benFeat = zip (map ben feats) feats- -- Probably recently switched levels or was pushed to another level.- -- Do not repeatedly switch levels or push each other between levels.- -- Consequently, AI won't dive many levels down with linked staircases.- -- TODO: beware of stupid monsters that backtrack and so occupy stairs.- recentlyAscended = bpos == boldpos- -- Too fast to notice and use features.- fast = bspeed b > speedNormal- if recentlyAscended || fast then- return mzero- else- return $ toFreq "triggerFreq" $ [ (benefit, TriggerSer aid (Just feat))- | (benefit, feat) <- benFeat- , benefit > 0 ]+ return $! toFreq "triggerFreq" $ [ (benefit, TriggerSer aid (Just feat))+ | (benefit, feat) <- benFeat+ , benefit > 0 ] -- Actors require sight to use ranged combat and intelligence to throw -- or zap anything else than obvious physical missiles.-rangedFreq :: MonadActionRO m- => Discovery -> ActorId -> Point -> m (Frequency CmdSerTakeTime)-rangedFreq disco aid fpos = do+rangedFreq :: MonadClient m+ => ActorId -> m (Frequency CmdTakeTimeSer)+rangedFreq aid = do cops@Kind.COps{ coactor=Kind.Ops{okind}- , coitem=Kind.Ops{okind=iokind}+ , coitem=coitem@Kind.Ops{okind=iokind} , corule- , cotile } <- getsState scops+ btarget <- getsClient $ getTarget aid b@Actor{bkind, bpos, bfid, blid, bbag, binv} <- getsState $ getActorBody aid- lvl@Level{lxsize, lysize} <- getLevel blid- let mk = okind bkind- tis = lvl `atI` bpos- fact <- getsState $ \s -> sfactionD s EM.! bfid- foes <- getsState $ actorNotProjList (isAtWar fact) blid- let foesAdj = foesAdjacent lxsize lysize bpos foes- posClear pos1 = Tile.hasFeature cotile F.Clear (lvl `at` pos1)- as <- getsState $ actorList (const True) blid- -- TODO: also don't throw if any pos on path is visibly not accessible- -- from previous (and tweak eps in bla to make it accessible).- -- Also don't throw if target not in range.- s <- getState+ mfpos <- aidTgtToPos aid blid btarget+ case (btarget, mfpos) of+ (Just TEnemy{}, Just fpos) -> do+ disco <- getsClient sdisco+ itemD <- getsState sitemD+ lvl@Level{lxsize, lysize} <- getLevel blid+ let mk = okind bkind+ tis = lvl `atI` bpos+ fact <- getsState $ \s -> sfactionD s EM.! bfid+ foes <- getsState $ actorNotProjList (isAtWar fact) blid+ let foesAdj = foesAdjacent lxsize lysize bpos foes+ (steps, eps) <- makePath b fpos+ let permitted = (if aiq mk >= 10 then ritemProject else ritemRanged)+ $ Kind.stdRuleset corule+ itemReaches item =+ let lingerPercent = isLingering coitem disco item+ toThrow = maybe 0 (itoThrow . iokind) $ jkind disco item+ speed = speedFromWeight (jweight item) toThrow+ range = rangeFromSpeed speed+ totalRange = lingerPercent * range `div` 100+ in steps <= totalRange -- probably enough range+ -- TODO: make sure itoThrow identified after a single throw+ getItemB iid =+ fromMaybe (assert `failure` "item body not found"+ `twith` (iid, itemD)) $ EM.lookup iid itemD+ throwFreq bag multi container =+ [ (- benefit * multi,+ ProjectSer aid fpos eps iid (container iid))+ | (iid, i) <- map (\iid -> (iid, getItemB iid))+ $ EM.keys bag+ , let benefit =+ case jkind disco i of+ Nothing -> -- TODO: (undefined, 0) --- for now, cheating+ effectToBenefit cops b (jeffect i)+ Just _ki ->+ let _kik = iokind _ki+ _unneeded = isymbol _kik+ in effectToBenefit cops b (jeffect i)+ , benefit < 0+ , jsymbol i `elem` permitted+ , itemReaches i ]+ freq =+ if asight mk -- ProjectBlind+ && not foesAdj -- ProjectBlockFoes+ -- ProjectAimOnself, ProjectBlockActor, ProjectBlockTerrain+ -- and no actors or obstracles along the path+ && steps == chessDist bpos fpos+ then toFreq "throwFreq"+ $ throwFreq bbag 4 (actorContainer aid binv)+ ++ throwFreq tis 8 (const $ CFloor blid bpos)+ else toFreq "throwFreq: not possible" []+ return $! freq+ _ -> return $! toFreq "throwFreq: no enemy target" []++-- TODO: finetune eps+-- | Counts the number of steps until the projectile would hit+-- an actor or obstacle.+makePath :: MonadClient m => Actor -> Point -> m (Int, Int)+makePath body fpos = do+ cops <- getsState scops+ lvl@Level{lxsize, lysize} <- getLevel (blid body)+ bs <- getsState $ actorNotProjList (const True) (blid body) let eps = 0- bl = bla lxsize lysize eps bpos fpos -- TODO:make an arg of projectGroupItem- permitted = (if aiq mk >= 10 then ritemProject else ritemRanged)- $ Kind.stdRuleset corule- throwFreq bag multi container =- [ (- benefit * multi,- ProjectSer aid fpos eps iid (container iid))- | (iid, i) <- map (\iid -> (iid, getItemBody iid s))- $ EM.keys bag- , let benefit =- case jkind disco i of- Nothing -> -- TODO: (undefined, 0) --- for now, cheating- effectToBenefit cops b (jeffect i)- Just _ki ->- let _kik = iokind _ki- _unneeded = isymbol _kik- in effectToBenefit cops b (jeffect i)- , benefit < 0- , jsymbol i `elem` permitted ]- return $ toFreq "throwFreq" $- case bl of- Just (pos1 : _) ->- if not foesAdj -- ProjectBlockFoes- && asight mk- && posClear pos1 -- ProjectBlockTerrain- && unoccupied as pos1 -- ProjectBlockActor- then throwFreq bbag 3 (actorContainer aid binv)- ++ throwFreq tis 6 (const $ CFloor blid bpos)- else []- _ -> [] -- ProjectAimOnself+ mbl = bla lxsize lysize eps (bpos body) fpos+ case mbl of+ Just bl@(pos1:_) -> do+ let noActor p = any ((== p) . bpos) bs+ case break noActor bl of+ (flies, hits : _) -> do+ let blRest = flies ++ [hits]+ blZip = zip (bpos body : blRest) blRest+ blAccess = takeWhile (uncurry $ accessible cops lvl) blZip+ mab <- getsState $ posToActor pos1 (blid body)+ if maybe True (bproj . snd . fst) mab then+ return $ (length blAccess, eps)+ else return (0, eps) -- ProjectBlockActor+ _ -> assert `failure` (body, fpos, bl)+ Just [] -> assert `failure` (body, fpos)+ Nothing -> return (0, eps) -- ProjectAimOnself -- Tools use requires significant intelligence and sometimes literacy. toolsFreq :: MonadActionRO m- => Discovery -> ActorId -> m (Frequency CmdSerTakeTime)+ => Discovery -> ActorId -> m (Frequency CmdTakeTimeSer) toolsFreq disco aid = do cops@Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops b@Actor{bkind, bpos, blid, bbag, binv} <- getsState $ getActorBody aid@@ -340,152 +537,97 @@ Just _ki -> effectToBenefit cops b $ jeffect i , benefit > 0 , jsymbol i `elem` mastered ]- return $ toFreq "useFreq" $+ return $! toFreq "useFreq" $ useFreq bbag 1 (actorContainer aid binv) ++ useFreq tis 2 (const $ CFloor blid bpos) --- TODO: express fully in MonadActionRO--- TODO: separate out bumping into solid tiles--- TODO: also close doors; then stupid members of the party won't see them,--- but it's assymetric warfare: rather harm humans than help party members--- | AI finds interesting moves in the absense of visible foes.--- This strategy can be null (e.g., if the actor is blocked by friends).-moveStrategy :: MonadActionRO m- => Kind.COps -> ActorId -> State -> Maybe (Point, Bool)- -> m (Strategy Vector)-moveStrategy cops aid s mFoe =- return $ case mFoe of- -- Target set and we chase the foe or his last position or another target.- Just (fpos, _) ->- let towardsFoe =- let tolerance | adjacent lxsize bpos fpos = 0- | otherwise = 1- foeDir = towards lxsize bpos fpos- in only (\x -> euclidDistSq lxsize foeDir x <= tolerance)- in if fpos == bpos- then reject- else towardsFoe- $ if foeVisible- then moveClear -- enemies in sight, don't waste time for doors- .| moveOpenable- else moveOpenable -- no enemy in sight, explore doors- .| moveClear- Nothing ->- let smells =- map (map fst)- $ groupBy ((==) `on` snd)- $ sortBy (flip compare `on` snd)- $ filter (\(_, sm) -> sm > timeZero)- $ map (\x ->- let sml = EM.findWithDefault- timeZero (bpos `shift` x) lsmell- in (x, sml `timeAdd` timeNegate ltime))- sensible- in asmell mk .=> foldr ((.|)- . liftFrequency- . uniformFreq "smell k") reject smells- .| moveOpenable -- no enemy in sight, explore doors- .| moveClear- where- Kind.COps{cotile, coactor=Kind.Ops{okind}} = cops- lvl@Level{lsmell, lxsize, lysize, ltime} = sdungeon s EM.! blid- Actor{bkind, bpos, boldpos, bfid, blid} = getActorBody aid s- mk = okind bkind- lootHere x = not $ EM.null $ lvl `atI` x- onlyLoot = onlyMoves lootHere- interestHere x = let t = lvl `at` x- ts = map (lvl `at`) $ vicinity lxsize lysize x- in Tile.hasFeature cotile F.Exit t- -- Blind actors tend to reveal/forget repeatedly.- || asight mk && Tile.hasFeature cotile F.Suspect t- -- Lit indirectly. E.g., a room entrance.- || (not (Tile.hasFeature cotile F.Lit t)- && (x == bpos || accessible cops lvl x bpos)- && any (Tile.hasFeature cotile F.Lit) ts)- onlyInterest = onlyMoves interestHere- bdirAI | bpos == boldpos = Nothing- | otherwise = Just $ towards lxsize boldpos bpos- onlyKeepsDir k =- only (\x -> maybe True (\d -> euclidDistSq lxsize d x <= k) bdirAI)- onlyKeepsDir_9 = only (\x -> maybe True (\d -> neg x /= d) bdirAI)- foeVisible = fmap snd mFoe == Just True- -- TODO: aiq 16 leads to robotic, repetitious, looping movement;- -- either base it off some new stat or wait until pathfinding,- -- which will eliminate the loops- moveIQ | foeVisible = onlyKeepsDir_9 moveRandomly -- danger, be flexible- | otherwise =- aiq mk > 15 .=> onlyKeepsDir 0 moveRandomly- .| aiq mk > 10 .=> onlyKeepsDir 1 moveRandomly- .| aiq mk > 5 .=> onlyKeepsDir 2 moveRandomly- .| onlyKeepsDir_9 moveRandomly- interestFreq | interestHere bpos =- -- Don't detour towards an interest if already on one.- mzero- | otherwise =- -- Prefer interests, but don't exclude other focused moves.- scaleFreq 10 $ bestVariant $ onlyInterest $ onlyKeepsDir 2 moveRandomly- interestIQFreq = interestFreq `mplus` bestVariant moveIQ- moveClear =- onlyMoves (not . bumpableHere cops lvl foeVisible (asight mk)) moveFreely- moveOpenable =- onlyMoves (bumpableHere cops lvl foeVisible (asight mk)) moveFreely- -- Ignore previously ignored loot, to prevent repetition.- moveNewLoot = onlyLoot (onlyKeepsDir 2 moveRandomly)- moveFreely = moveNewLoot- .| liftFrequency interestIQFreq- .| moveIQ -- @bestVariant moveIQ@ may be excluded elsewhere- .| moveRandomly- onlyMoves :: (Point -> Bool) -> Strategy Vector -> Strategy Vector- onlyMoves p = only (\x -> p (bpos `shift` x))- moveRandomly :: Strategy Vector- moveRandomly = liftFrequency $ uniformFreq "moveRandomly" sensible- accessibleHere = accessible cops lvl bpos- fact = sfactionD s EM.! bfid- friends = actorList (not . isAtWar fact) blid s- noFriends | asight mk = unoccupied friends- | otherwise = const True- isSensible l = noFriends l- && (accessibleHere l- || bumpableHere cops lvl foeVisible (asight mk) l)- sensible = filter (isSensible . (bpos `shift`)) (moves lxsize)--bumpableHere :: Kind.COps -> Level -> Bool -> Bool -> Point -> Bool-bumpableHere Kind.COps{cotile} lvl foeVisible asight pos =- let t = lvl `at` pos -- cannot hold items, so OK- in Tile.openable cotile t- || -- Try to find hidden doors only if no foe in sight and not blind.- -- Blind actors forget their search results too quickly.- asight && not foeVisible && Tile.hasFeature cotile F.Suspect t+displace :: MonadClient m => ActorId -> m (Strategy CmdTakeTimeSer)+displace aid = do+ mtgtMPath <- getsClient $ EM.lookup aid . stargetD+ str <- case mtgtMPath of+ Just (_, Just (p : q : _, _)) -> displaceTowards aid p q+ _ -> return reject -- goal reached+ Traversable.mapM (moveOrRunAid True aid) str -chase :: MonadActionRO m- => ActorId -> (Point, Bool) -> m (Strategy CmdSerTakeTime)-chase aid foe@(_, foeVisible) = do+-- TODO: perhaps modify target when actually moving, not when+-- producing the strategy, even if it's a unique choice in this case.+displaceTowards :: MonadClient m+ => ActorId -> Point -> Point -> m (Strategy Vector)+displaceTowards aid source target = do cops <- getsState scops- -- Target set and we chase the foe or offer null strategy if we can't.- -- The foe is visible, or we remember his last position.- let mFoe = Just foe- fight = not foeVisible -- don't pick fights if the real foe is close- s <- getState- str <- moveStrategy cops aid s mFoe- if fight- then Traversable.mapM (moveRunAid False aid) str- else Traversable.mapM (moveRunAid True aid) str+ b <- getsState $ getActorBody aid+ assert (source == bpos b && adjacent source target) skip+ lvl <- getsState $ (EM.! blid b) . sdungeon+ if boldpos b /= target -- avoid trivial loops+ && accessible cops lvl source target then do+ mBlocker <- getsState $ posToActors target (blid b)+ case mBlocker of+ [] -> return reject+ [((aid2, _), _)] -> do+ mtgtMPath <- getsClient $ EM.lookup aid2 . stargetD+ case mtgtMPath of+ Just (tgt, Just (p : q : rest, (goal, len)))+ | q == source && p == target -> do+ let newTgt = Just (tgt, Just (q : rest, (goal, len - 1)))+ modifyClient $ \cli ->+ cli {stargetD = EM.alter (const $ newTgt) aid (stargetD cli)}+ return $! returN "displace friend" $ displacement source target+ Just _ -> return reject+ Nothing ->+ return $! returN "displace other" $ displacement source target+ _ -> return reject -- many projectiles, can't displace+ else return reject -wander :: MonadActionRO m- => ActorId -> m (Strategy CmdSerTakeTime)-wander aid = do- cops <- getsState scops- -- Target set, but we don't chase the foe, e.g., because we are blocked- -- or we cannot chase at all.- let mFoe = Nothing- s <- getState- str <- moveStrategy cops aid s mFoe- Traversable.mapM (moveRunAid False aid) str+chase :: MonadClient m => ActorId -> Bool -> m (Strategy CmdTakeTimeSer)+chase aid foeVisible = do+ mtgtMPath <- getsClient $ EM.lookup aid . stargetD+ str <- case mtgtMPath of+ Just (_, Just (p : q : _, (goal, _))) -> moveTowards aid p q goal+ _ -> return reject -- goal reached+ if foeVisible -- don't pick fights, but displace, if the real foe is close+ then Traversable.mapM (moveOrRunAid True aid) str+ else Traversable.mapM (moveOrRunAid False aid) str +moveTowards :: MonadClient m+ => ActorId -> Point -> Point -> Point -> m (Strategy Vector)+moveTowards aid source target goal = do+ cops@Kind.COps{coactor=Kind.Ops{okind}, cotile} <- getsState scops+ b <- getsState $ getActorBody aid+ assert (source == bpos b && adjacent source target) skip+ lvl <- getsState $ (EM.! blid b) . sdungeon+ fact <- getsState $ (EM.! bfid b) . sfactionD+ friends <- getsState $ actorList (not . isAtWar fact) $ blid b+ let _mk = okind $ bkind b+ noFriends = unoccupied friends+ -- Was:+ -- noFriends | asight mk = unoccupied friends+ -- | otherwise = const True+ -- but this should be implemented on the server or, if not,+ -- restricted to AI-only factions (e.g., animals).+ -- Otherwise human players are tempted to tweak their AI clients+ -- (as soon as we let them register their AI clients with the server).+ accessibleHere = accessible cops lvl source+ bumpableHere p =+ let t = lvl `at` p+ in Tile.isOpenable cotile t || Tile.isSuspect cotile t+ enterableHere p = accessibleHere p || bumpableHere p+ if noFriends target && enterableHere target then+ return $! returN "moveTowards adjacent" $ displacement source target+ else do+ let goesBack v = v == displacement source (boldpos b)+ nonincreasing p = chessDist source goal >= chessDist p goal+ isSensible p = nonincreasing p && noFriends p && enterableHere p+ sensible = [ ((goesBack v, chessDist p goal), v)+ | v <- moves, let p = source `shift` v, isSensible p ]+ sorted = sortBy (comparing fst) sensible+ groups = map (map snd) $ groupBy ((==) `on` fst) sorted+ freqs = map (liftFrequency . uniformFreq "moveTowards") groups+ return $! foldr (.|) reject freqs+ -- | Actor moves or searches or alters or attacks. Displaces if @run@.-moveRunAid :: MonadActionRO m- => Bool -> ActorId -> Vector -> m CmdSerTakeTime-moveRunAid run source dir = do+moveOrRunAid :: MonadActionRO m+ => Bool -> ActorId -> Vector -> m CmdTakeTimeSer+moveOrRunAid run source dir = do cops@Kind.COps{cotile} <- getsState scops sb <- getsState $ getActorBody source let lid = blid sb@@ -497,33 +639,35 @@ -- which gives a partial information (actors can be invisible), -- as opposed to accessibility (and items) which are always accurate -- (tiles can't be invisible).- tgt <- getsState $ posToActor tpos lid- case tgt of- Just target | run -> -- can be a foe, as well as a friend+ tgts <- getsState $ posToActors tpos lid+ case tgts of+ [((target, _), _)] | run -> -- can be a foe, as well as a friend if accessible cops lvl spos tpos then -- Displacing requires accessibility.- return $ DisplaceSer source target+ return $! DisplaceSer source target else -- If cannot displace, hit. No DisplaceAccess.- return $ MeleeSer source target- Just target -> -- can be a foe, as well as a friend+ return $! MeleeSer source target+ ((target, _), _) : _ -> -- can be a foe, as well as a friend (e.g., proj.)+ -- No problem if there are many projectiles at the spot. We just+ -- attack the first one. -- Attacking does not require full access, adjacency is enough.- return $ MeleeSer source target- Nothing -> do -- move or search or alter+ return $! MeleeSer source target+ [] -> do -- move or search or alter if accessible cops lvl spos tpos then -- Movement requires full access.- return $ MoveSer source dir+ return $! MoveSer source dir -- The potential invisible actor is hit. else if not $ EM.null $ lvl `atI` tpos then -- This is, e.g., inaccessible open door with an item in it. assert `failure` "AI causes AlterBlockItem" `twith` (run, source, dir)- else if not (Tile.hasFeature cotile F.Walkable t) -- not implied- && (Tile.hasFeature cotile F.Suspect t- || Tile.openable cotile t- || Tile.closable cotile t- || Tile.changeable cotile t) then+ else if not (Tile.isWalkable cotile t) -- not implied+ && (Tile.isSuspect cotile t+ || Tile.isOpenable cotile t+ || Tile.isClosable cotile t+ || Tile.isChangeable cotile t) then -- No access, so search and/or alter the tile.- return $ AlterSer source tpos Nothing+ return $! AlterSer source tpos Nothing else -- Boring tile, no point bumping into it, do WaitSer if really idle. assert `failure` "AI causes MoveNothing or AlterNothing"@@ -535,7 +679,6 @@ effectToBenefit :: Kind.COps -> Actor -> Effect.Effect Int -> Int effectToBenefit Kind.COps{coactor=Kind.Ops{okind}} b eff = let kind = okind $ bkind b- deep k = signum k == signum (fromEnum $ blid b) in case eff of Effect.NoEffect -> 0 (Effect.Heal p) -> 10 * min p (Random.maxDice (ahp kind) - bhp b)@@ -548,6 +691,5 @@ Effect.ApplyPerfume -> 0 Effect.Regeneration{} -> 0 -- bigger benefit from carrying around Effect.Searching{} -> 0- (Effect.Ascend k) | deep k -> 500 -- AI likes to explore deep down- Effect.Ascend{} -> 1- Effect.Escape -> 1000 -- AI wants to win+ Effect.Ascend{} -> 0 -- change levels sensibly, in teams+ Effect.Escape{} -> 10000 -- AI wants to win; spawners to guard
Game/LambdaHack/Common/Ability.hs view
@@ -10,10 +10,11 @@ -- that any actor picks each turn, depending on the actor's characteristics -- and his environment. data Ability =- Track -- ^ move along a set path, if any, meleeing any opponents+ Track -- ^ move along a set trajectory, if any, meleeing any opponents | Heal -- ^ heal if almost dead | Flee -- ^ flee if almost dead | Melee -- ^ melee target+ | Displace -- ^ switch places with a friend | Pickup -- ^ gather items, if no foes visible | Trigger -- ^ trigger a feature | Ranged -- ^ attack the visible target opponent at range, some of the time
Game/LambdaHack/Common/Action.hs view
@@ -10,9 +10,7 @@ , serverSaveName ) where -import Control.Monad.Writer.Strict (WriterT, lift) import qualified Data.EnumMap.Strict as EM-import Data.Monoid import Game.LambdaHack.Common.AtomicCmd import Game.LambdaHack.Common.Faction@@ -24,13 +22,6 @@ getState :: m State getsState :: (State -> a) -> m a -instance (Monoid a, MonadActionRO m) => MonadActionRO (WriterT a m) where- getState = lift getState- getsState = lift . getsState--instance MonadActionRO m => Show (WriterT a m b) where- show _ = "an action"- class MonadActionRO m => MonadAction m where modifyState :: (State -> State) -> m () putState :: State -> m ()@@ -48,7 +39,7 @@ nUI :: MonadActionRO m => m Int nUI = do factionD <- getsState sfactionD- return $ length $ filter (playerUI . gplayer) $ EM.elems factionD+ return $! length $ filter (playerUI . gplayer) $ EM.elems factionD serverSaveName :: String serverSaveName = "server.sav"
Game/LambdaHack/Common/Actor.hs view
@@ -11,9 +11,10 @@ , ItemBag, ItemInv, InvChar(..), ItemDict, ItemRev , allLetters, assignLetter, letterLabel, letterRange, rmFromBag -- * Assorted- , ActorDict, smellTimeout, mapActorItems_+ , ActorDict, smellTimeout, mapActorItems_, checkAdjacent ) where +import Control.Exception.Assert.Sugar import Data.Binary import Data.Char import qualified Data.EnumMap.Strict as EM@@ -37,7 +38,6 @@ import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ActorKind-import Control.Exception.Assert.Sugar -- | A unique identifier of an actor in the dungeon. newtype ActorId = ActorId Int@@ -52,24 +52,25 @@ -- they are usually modified temporarily, but tend to return -- to the original value from @ActorKind@ over time. E.g., HP. data Actor = Actor- { bkind :: !(Kind.Id ActorKind) -- ^ the kind of the actor- , bsymbol :: !Char -- ^ individual map symbol- , bname :: !Text -- ^ individual name- , bcolor :: !Color.Color -- ^ individual map color- , bspeed :: !Speed -- ^ individual speed- , bhp :: !Int -- ^ current hit points- , bpath :: !(Maybe [Vector]) -- ^ path the actor is forced to travel- , bpos :: !Point -- ^ current position- , boldpos :: !Point -- ^ previous position- , blid :: !LevelId -- ^ current level- , bbag :: !ItemBag -- ^ items carried- , binv :: !ItemInv -- ^ map from letters to items- , bletter :: !InvChar -- ^ next inventory letter- , btime :: !Time -- ^ absolute time of next action- , bwait :: !Time -- ^ last bracing expires at this time- , bfid :: !FactionId -- ^ to which faction the actor belongs- , bproj :: !Bool -- ^ is a projectile? (shorthand only,- -- this can be deduced from bkind)+ { bkind :: !(Kind.Id ActorKind) -- ^ the kind of the actor+ , bsymbol :: !Char -- ^ individual map symbol+ , bname :: !Text -- ^ individual name+ , bcolor :: !Color.Color -- ^ individual map color+ , bspeed :: !Speed -- ^ individual speed+ , bhp :: !Int -- ^ current hit points+ , btrajectory :: !(Maybe [Vector]) -- ^ trajectory the actor must travel+ , bpos :: !Point -- ^ current position+ , boldpos :: !Point -- ^ previous position+ , blid :: !LevelId -- ^ current level+ , boldlid :: !LevelId -- ^ previous level+ , bbag :: !ItemBag -- ^ items carried+ , binv :: !ItemInv -- ^ map from letters to items+ , bletter :: !InvChar -- ^ next inventory letter+ , btime :: !Time -- ^ absolute time of next action+ , bwait :: !Bool -- ^ is the actor waiting right now?+ , bfid :: !FactionId -- ^ faction the actor belongs to+ , bproj :: !Bool -- ^ is a projectile? (shorthand only,+ -- this can be deduced from bkind) } deriving (Show, Eq, Ord) @@ -84,8 +85,9 @@ -- Mimics @castDeep@. let n = abs n' depth = abs depth'- scaledDepth = 10 * (n - 1) `div` max 1 (depth - 1)- in chance $ 1%(fromIntegral (50 * (numMonsters - scaledDepth)) `max` 5)+ -- On level 1, First 2 monsters appear fast.+ scaledDepth = 5 * n `div` depth+ in chance $ 1%(fromIntegral (100 * (numMonsters - scaledDepth)) `max` 10) -- | The part of speech describing the actor. partActor :: Actor -> MU.Part@@ -97,13 +99,14 @@ actorTemplate :: Kind.Id ActorKind -> Char -> Text -> Color.Color -> Speed -> Int -> Maybe [Vector] -> Point -> LevelId -> Time -> FactionId -> Bool -> Actor-actorTemplate bkind bsymbol bname bcolor bspeed bhp bpath bpos blid btime+actorTemplate bkind bsymbol bname bcolor bspeed bhp btrajectory bpos blid btime bfid bproj =- let boldpos = bpos+ let boldpos = Point 0 0 -- make sure /= bpos, to tell it didn't switch level+ boldlid = blid bbag = EM.empty binv = EM.empty bletter = InvChar 'a'- bwait = timeZero+ bwait = False in Actor{..} -- | Add time taken by a single step at the actor's current speed.@@ -113,17 +116,13 @@ delta = ticksPerMeter speed in timeAdd time delta --- | Whether an actor is braced for combat this clip. If a foe--- moves just after this actor in the same time moment, the actor won't block,--- because @bwait@ is reset to zero in @ageActorA@ before the condition--- is checked.-braced :: Actor -> Time -> Bool-braced b time = time <= bwait b+-- | Whether an actor is braced for combat this clip.+braced :: Actor -> Bool+braced b = bwait b --- | The actor most probably waited at most a turn ago (unless his speed--- was changed, etc.)-waitedLastTurn :: Actor -> Time -> Bool-waitedLastTurn b time = time <= bwait b+-- | The actor waited last turn.+waitedLastTurn :: Actor -> Bool+waitedLastTurn b = bwait b -- | Checks for the presence of actors in a position. -- Does not check if the tile is walkable.@@ -228,6 +227,9 @@ let is = EM.assocs bbag mapM_ (uncurry f) is +checkAdjacent :: Actor -> Actor -> Bool+checkAdjacent sb tb = blid sb == blid tb && adjacent (bpos sb) (bpos tb)+ instance Binary Actor where put Actor{..} = do put bkind@@ -236,10 +238,11 @@ put bcolor put bspeed put bhp- put bpath+ put btrajectory put bpos put boldpos put blid+ put boldlid put bbag put binv put bletter@@ -254,10 +257,11 @@ bcolor <- get bspeed <- get bhp <- get- bpath <- get+ btrajectory <- get bpos <- get boldpos <- get blid <- get+ boldlid <- get bbag <- get binv <- get bletter <- get@@ -265,4 +269,4 @@ bwait <- get bfid <- get bproj <- get- return Actor{..}+ return $! Actor{..}
Game/LambdaHack/Common/ActorState.hs view
@@ -2,13 +2,17 @@ -- but not the 'Action' type. -- TODO: Document an export list after it's rewritten according to #17. module Game.LambdaHack.Common.ActorState- ( actorAssocs, actorList, actorNotProjAssocs, actorNotProjList+ ( actorAssocsLvl, actorAssocs, actorList+ , actorNotProjAssocsLvl, actorNotProjAssocs, actorNotProjList , calculateTotal, nearbyFreePoints, whereTo- , posToActor, getItemBody, memActor, getActorBody, updateActorBody- , getActorItem, getActorBag, actorContainer, getActorInv+ , posToActors, posToActor, getItemBody, memActor+ , getActorBody, updateActorBody+ , getActorItem, getFloorItem, getActorBag+ , actorContainer, actorContainerB, getActorInv , tryFindHeroK, foesAdjacent ) where +import Control.Exception.Assert.Sugar import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES@@ -17,55 +21,67 @@ import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.Faction-import qualified Game.LambdaHack.Common.Feature as F import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.TileKind-import Control.Exception.Assert.Sugar -actorAssocs :: (FactionId -> Bool) -> LevelId -> State- -> [(ActorId, Actor)]-actorAssocs p lid s =- mapMaybe (\aid -> let actor = sactorD s EM.! aid+actorAssocsLvl :: (FactionId -> Bool) -> Level -> ActorDict+ -> [(ActorId, Actor)]+actorAssocsLvl p lvl actorD =+ mapMaybe (\aid -> let actor = actorD EM.! aid in if p (bfid actor) then Just (aid, actor) else Nothing)- $ concat $ EM.elems $ lprio $ sdungeon s EM.! lid+ $ concat $ EM.elems $ lprio lvl +actorAssocs :: (FactionId -> Bool) -> LevelId -> State+ -> [(ActorId, Actor)]+actorAssocs p lid s =+ actorAssocsLvl p (sdungeon s EM.! lid) (sactorD s)+ actorList :: (FactionId -> Bool) -> LevelId -> State -> [Actor] actorList p lid s = map snd $ actorAssocs p lid s -actorNotProjAssocs :: (FactionId -> Bool) -> LevelId -> State- -> [(ActorId, Actor)]-actorNotProjAssocs p lid s =- mapMaybe (\aid -> let actor = sactorD s EM.! aid+actorNotProjAssocsLvl :: (FactionId -> Bool) -> Level -> ActorDict+ -> [(ActorId, Actor)]+actorNotProjAssocsLvl p lvl actorD =+ mapMaybe (\aid -> let actor = actorD EM.! aid in if not (bproj actor) && p (bfid actor) then Just (aid, actor) else Nothing)- $ concat $ EM.elems $ lprio $ sdungeon s EM.! lid+ $ concat $ EM.elems $ lprio lvl +actorNotProjAssocs :: (FactionId -> Bool) -> LevelId -> State+ -> [(ActorId, Actor)]+actorNotProjAssocs p lid s =+ actorNotProjAssocsLvl p (sdungeon s EM.! lid) (sactorD s)+ actorNotProjList :: (FactionId -> Bool) -> LevelId -> State -> [Actor] actorNotProjList p lid s = map snd $ actorNotProjAssocs p lid s --- | Finds an actor at a position on the current level. Perception irrelevant.-posToActor :: Point -> LevelId -> State -> Maybe ActorId-posToActor pos lid s =- let l = posToActors pos lid s- in assert (length l <= 1 `blame` "many actors at the same position" `twith` l)- $ listToMaybe l+-- | Finds an actor at a position on the current level.+posToActor :: Point -> LevelId -> State+ -> Maybe ((ActorId, Actor), [(ItemId, Item)])+posToActor pos lid s = listToMaybe $ posToActors pos lid s -posToActors :: Point -> LevelId -> State -> [ActorId]+posToActors :: Point -> LevelId -> State+ -> [((ActorId, Actor), [(ItemId, Item)])] posToActors pos lid s = let as = actorAssocs (const True) lid s- apos = filter (\(_, b) -> bpos b == pos) as- in fmap fst apos+ aps = filter (\(_, b) -> bpos b == pos) as+ f iid = (iid, getItemBody iid s)+ g (aid, b) = ((aid, b), map f $ EM.keys $ bbag b)+ l = map g aps+ in assert (length l <= 1 || all (bproj . snd . fst) l+ `blame` "many actors at the same position" `twith` l)+ l nearbyFreePoints :: Kind.Ops TileKind -> (Kind.Id TileKind -> Bool) -> Point -> LevelId -> State@@ -74,7 +90,7 @@ let lvl@Level{lxsize, lysize} = sdungeon s EM.! lid as = actorList (const True) lid s good p = f (lvl `at` p)- && Tile.hasFeature cotile F.Walkable (lvl `at` p)+ && Tile.isWalkable cotile (lvl `at` p) && unoccupied as p ps = nub $ start : concatMap (vicinity lxsize lysize) ps in filter good ps@@ -88,9 +104,9 @@ calculateTotal body s = let bs = actorList (== bfid body) (blid body) s bag = EM.unionsWith (+) $ map bbag $ if null bs then [body] else bs- heroItem = map (\(iid, k) -> (getItemBody iid s, k))- $ EM.assocs bag- in (bag, sum $ map itemPrice heroItem)+ items = map (\(iid, k) -> (getItemBody iid s, k))+ $ EM.assocs bag+ in (bag, sum $ map itemPrice items) -- | Price an item, taking count into consideration. itemPrice :: (Item, Int) -> Int@@ -127,17 +143,16 @@ whereTo :: LevelId -- ^ level of the stairs -> Point -- ^ position of the stairs -> Int -- ^ jump up this many levels- -> State -- ^ game state+ -> Dungeon -- ^ current game dungeon -> (LevelId, Point) -- ^ target level and the position of its receiving stairs-whereTo lid pos k s = assert (k /= 0) $- let dungeon = sdungeon s- lvl = dungeon EM.! lid+whereTo lid pos k dungeon = assert (k /= 0) $+ let lvl = dungeon EM.! lid stairs = (if k < 0 then snd else fst) (lstair lvl) defaultStairs = 0 -- for ascending via, e.g., spells mindex = elemIndex pos stairs i = fromMaybe defaultStairs mindex- in case ascendInBranch dungeon lid k of+ in case ascendInBranch dungeon k lid of [] | isNothing mindex -> (lid, pos) -- spell fizzles [] -> assert `failure` "no dungeon level to go to" `twith` (lid, pos, k) ln : _ -> let lvlTgt = dungeon EM.! ln@@ -170,6 +185,16 @@ Just (l, _) -> CActor aid l Nothing -> assert `failure` "item not in inventory" `twith` (aid, binv, iid) +actorContainerB :: ActorId -> Actor -> ItemId -> Item -> Maybe Container+actorContainerB aid body iid item =+ case find ((== iid) . snd) $ EM.assocs (binv body) of+ Just (l, _) -> Just $ CActor aid l+ Nothing ->+ let l = if jsymbol item == '$' then Just $ InvChar '$' else Nothing+ in case assignLetter iid l body of+ Just l2 -> Just $ CActor aid l2+ Nothing -> Nothing+ getActorInv :: ActorId -> State -> ItemInv getActorInv aid s = binv $ getActorBody aid s @@ -179,6 +204,11 @@ getActorItem aid s = let f iid = (iid, getItemBody iid s) in map f $ EM.keys $ getActorBag aid s++getFloorItem :: LevelId -> Point -> State -> [(ItemId, Item)]+getFloorItem lid pos s =+ let f iid = (iid, getItemBody iid s)+ in map f $ EM.keys $ sdungeon s EM.! lid `atI` pos getItemBody :: ItemId -> State -> Item getItemBody iid s =
Game/LambdaHack/Common/Animation.hs view
@@ -1,52 +1,82 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Screen frames and animations. module Game.LambdaHack.Common.Animation- ( Attr(..), defAttr, AttrChar(..)- , SingleFrame(..), emptySingleFrame, xsizeSingleFrame, ysizeSingleFrame+ ( SingleFrame(..), decodeLine, encodeLine+ , overlayOverlay, xsizeSingleFrame, ysizeSingleFrame , Animation, Frames, renderAnim, restrictAnim , twirlSplash, blockHit, blockMiss, deathBody, swapPlaces, fadeout , AcFrame(..) , DebugModeCli(..), defDebugModeCli ) where -import Control.Arrow ((***))+import Control.Exception.Assert.Sugar import Control.Monad import Data.Binary import Data.Bits import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import qualified Data.List as L+import Data.Int (Int32)+import Data.List import Data.Maybe import Data.Monoid-import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Vector.Generic as G+import qualified Data.Vector.Unboxed as U+import GHC.Generics (Generic) import Game.LambdaHack.Common.Color+import qualified Game.LambdaHack.Common.Color as Color import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY import Game.LambdaHack.Common.Random +type ScreenLine = U.Vector Int32++decodeLine :: ScreenLine -> [AttrChar]+decodeLine v = map (toEnum . fromIntegral) $ G.toList v++encodeLine :: [AttrChar] -> ScreenLine+encodeLine l = G.fromList $ map (fromIntegral . fromEnum) l+ -- | The data sufficent to draw a single game screen frame.------ The fields are not strict, because sometimes frames are not used,--- e.g., when a keypress discards all frames not yet drawn and displayed. data SingleFrame = SingleFrame- { sfLevel :: [[AttrChar]] -- ^ content of the screen, line by line- , sfTop :: Text -- ^ an extra line to show at the top- , sfBottom :: Text -- ^ an extra line to show at the bottom+ { sfLevel :: ![ScreenLine] -- ^ screen, from top to bottom, line by line+ , sfTop :: !Overlay -- ^ some extra lines to show over the top+ , sfBottom :: ![ScreenLine] -- ^ some extra lines to show at the bottom+ , sfBlank :: !Bool -- ^ display only @sfTop@, on blank screen } deriving (Eq, Show) -instance Binary SingleFrame where- put SingleFrame{..} = do- put sfLevel- put sfTop- put sfBottom- get = do- sfLevel <- get- sfTop <- get- sfBottom <- get- return SingleFrame{..}+-- | Overlays the @sfTop@ and @sfBottom@ fields onto the @sfLevel@ field.+-- The resulting frame has empty @sfTop@ and @sfBottom@.+-- To be used by simple frontends that don't display overlays+-- in separate windows/panes/scrolled views.+overlayOverlay :: SingleFrame -> SingleFrame+overlayOverlay sf@SingleFrame{..} =+ let lxsize = xsizeSingleFrame sf+ lysize = ysizeSingleFrame sf+ emptyLine = encodeLine+ $ replicate lxsize (Color.AttrChar Color.defAttr ' ')+ canvasLength = if sfBlank then lysize + 3 else lysize + 1+ canvas | sfBlank = replicate canvasLength emptyLine+ | otherwise = emptyLine : sfLevel+ topTrunc = map (truncateMsg lxsize) $ overlay sfTop+ topLayer = if length topTrunc <= canvasLength+ then topTrunc+ else take (canvasLength - 1) topTrunc+ ++ ["--a portion of the text trimmed--"]+ addAttr t = map (Color.AttrChar Color.defAttr) (T.unpack t)+ f layerLine canvasLine = encodeLine+ $ addAttr layerLine+ ++ drop (T.length layerLine) (decodeLine canvasLine)+ picture = zipWith f topLayer canvas+ bottomLines = if sfBlank then [] else sfBottom+ newLevel = picture ++ drop (length picture) canvas ++ bottomLines+ in SingleFrame { sfLevel = newLevel+ , sfTop = emptyOverlay+ , sfBottom = []+ , sfBlank } -- | Animation is a list of frame modifications to play one by one, -- where each modification if a map from positions to level map symbols.@@ -56,12 +86,9 @@ -- | Sequences of screen frames, including delays. type Frames = [Maybe SingleFrame] -emptySingleFrame :: SingleFrame-emptySingleFrame = SingleFrame{sfLevel = [], sfTop = "", sfBottom = ""}- xsizeSingleFrame :: SingleFrame -> X xsizeSingleFrame SingleFrame{sfLevel=[]} = 0-xsizeSingleFrame SingleFrame{sfLevel=line : _} = length line+xsizeSingleFrame SingleFrame{sfLevel=line : _} = G.length line ysizeSingleFrame :: SingleFrame -> X ysizeSingleFrame SingleFrame{sfLevel} = length sfLevel@@ -69,17 +96,21 @@ -- | Render animations on top of a screen frame. renderAnim :: X -> Y -> SingleFrame -> Animation -> Frames renderAnim lxsize lysize basicFrame (Animation anim) =- let modifyFrame SingleFrame{sfLevel = levelOld, ..} am =+ let modifyFrame SingleFrame{sfLevel = []} _ =+ assert `failure` (lxsize, lysize, basicFrame, anim)+ modifyFrame SingleFrame{sfLevel = levelOld, ..} am = let fLine y lineOld = let f l (x, acOld) =- let pos = toPoint lxsize (PointXY (x, y))+ let pos = Point x y !ac = fromMaybe acOld $ EM.lookup pos am in ac : l- in L.foldl' f [] (zip [lxsize-1,lxsize-2..0] (reverse lineOld))- sfLevel = -- Fully evaluated.+ in foldl' f [] (zip [lxsize-1,lxsize-2..0] (reverse lineOld))+ sfLevel = -- fully evaluated inside let f l (y, lineOld) = let !line = fLine y lineOld in line : l- in L.foldl' f [] (zip [lysize-1,lysize-2..0] (reverse levelOld))- in Just SingleFrame{..}+ in map encodeLine+ $ foldl' f [] (zip [lysize-1,lysize-2..0]+ $ reverse $ map decodeLine levelOld)+ in Just SingleFrame{..} -- a thunk within Just in map (modifyFrame basicFrame) anim blank :: Maybe AttrChar@@ -186,17 +217,17 @@ in EM.findWithDefault ' ' k edge rollFrame n = do r <- random- let l = [ ( PointXY (if topRight then x else xbound - x, y)- , fadeChar r n x y )+ let l = [ ( Point (if topRight then x else xbound - x) y+ , AttrChar defAttr $ fadeChar r n x y ) | x <- [0..xbound] , y <- [max 0 (ybound - (n - x) `div` 2)..ybound] ++ [0..min ybound ((n - xbound + x) `div` 2)] ]- return $ EM.fromList $ map (toPoint lxsize *** AttrChar defAttr) l+ return $! EM.fromList l startN = if out then 3 else 1 fs = [startN..3 * lxsize `divUp` 4 + 2] as <- mapM rollFrame $ if out then fs else reverse fs- return $ Animation as+ return $! Animation as data AcFrame = AcConfirm !SingleFrame@@ -205,20 +236,6 @@ | AcDelay deriving (Show, Eq) -instance Binary AcFrame where- put (AcConfirm fr) = putWord8 0 >> put fr- put (AcRunning fr) = putWord8 1 >> put fr- put (AcNormal fr) = putWord8 2 >> put fr- put AcDelay = putWord8 3- get = do- tag <- getWord8- case tag of- 0 -> liftM AcConfirm get- 1 -> liftM AcRunning get- 2 -> liftM AcNormal get- 3 -> return AcDelay- _ -> fail "no parse (AcFrame)"- data DebugModeCli = DebugModeCli { sfont :: !(Maybe String) -- ^ Font to use for the main game window.@@ -237,14 +254,18 @@ -- ^ Don't show any animations. , snewGameCli :: !Bool -- ^ Start a new game, overwriting the save file.+ , sdifficultyCli :: !Int+ -- ^ The difficulty level for all UI clients. , ssavePrefixCli :: !(Maybe String) -- ^ Prefix of the save game file. , sfrontendStd :: !Bool -- ^ Whether to use the stdout/stdin frontend.+ , sfrontendNo :: !Bool+ -- ^ Whether to use no frontend at all (for benchmarking, etc.). , sdbgMsgCli :: !Bool -- ^ Show clients' internal debug messages. }- deriving (Show, Eq)+ deriving (Show, Eq, Generic) defDebugModeCli :: DebugModeCli defDebugModeCli = DebugModeCli@@ -254,30 +275,38 @@ , snoMore = False , snoAnim = Nothing , snewGameCli = False+ , sdifficultyCli = 0 , ssavePrefixCli = Nothing , sfrontendStd = False+ , sfrontendNo = False , sdbgMsgCli = False } -instance Binary DebugModeCli where- put DebugModeCli{..} = do- put sfont- put smaxFps- put snoDelay- put snoMore- put snoAnim- put snewGameCli- put ssavePrefixCli- put sfrontendStd- put sdbgMsgCli+instance Binary AcFrame where+ put (AcConfirm fr) = putWord8 0 >> put fr+ put (AcRunning fr) = putWord8 1 >> put fr+ put (AcNormal fr) = putWord8 2 >> put fr+ put AcDelay = putWord8 3 get = do- sfont <- get- smaxFps <- get- snoDelay <- get- snoMore <- get- snoAnim <- get- snewGameCli <- get- ssavePrefixCli <- get- sfrontendStd <- get- sdbgMsgCli <- get- return DebugModeCli{..}+ tag <- getWord8+ case tag of+ 0 -> liftM AcConfirm get+ 1 -> liftM AcRunning get+ 2 -> liftM AcNormal get+ 3 -> return AcDelay+ _ -> fail "no parse (AcFrame)"++instance Binary SingleFrame where+ put SingleFrame{..} = do+ put sfLevel+ put sfTop+ put sfBottom+ put sfBlank+ get = do+ sfLevel <- get+ sfTop <- get+ sfBottom <- get+ sfBlank <- get+ return $! SingleFrame{..}++instance Binary DebugModeCli
Game/LambdaHack/Common/AtomicCmd.hs view
@@ -59,14 +59,14 @@ | LoseItemA !ItemId !Item !Int !Container -- Move actors and items. | MoveActorA !ActorId !Point !Point- | WaitActorA !ActorId !Time !Time+ | WaitActorA !ActorId !Bool !Bool | DisplaceActorA !ActorId !ActorId | MoveItemA !ItemId !Int !Container !Container -- Change actor attributes. | AgeActorA !ActorId !Time | HealActorA !ActorId !Int | HasteActorA !ActorId !Speed- | PathActorA !ActorId !(Maybe [Vector]) !(Maybe [Vector])+ | TrajectoryActorA !ActorId !(Maybe [Vector]) !(Maybe [Vector]) | ColorActorA !ActorId !Color.Color !Color.Color -- Change faction attributes. | QuitFactionA !FactionId !(Maybe Actor) !(Maybe Status) !(Maybe Status)@@ -85,7 +85,7 @@ | AgeGameA !Time | DiscoverA !LevelId !Point !ItemId !(Kind.Id ItemKind) | CoverA !LevelId !Point !ItemId !(Kind.Id ItemKind)- | PerceptionA !LevelId !PerActor !PerActor+ | PerceptionA !LevelId !Perception !Perception | RestartA !FactionId !Discovery !FactionPers !State !DebugModeCli !Text | RestartServerA !State | ResumeA !FactionId !FactionPers@@ -138,7 +138,7 @@ AgeActorA aid t -> Just $ AgeActorA aid (timeNegate t) HealActorA aid n -> Just $ HealActorA aid (-n) HasteActorA aid delta -> Just $ HasteActorA aid (speedNegate delta)- PathActorA aid fromPath toPath -> Just $ PathActorA aid toPath fromPath+ TrajectoryActorA aid fromT toT -> Just $ TrajectoryActorA aid toT fromT ColorActorA aid fromCol toCol -> Just $ ColorActorA aid toCol fromCol QuitFactionA fid mb fromSt toSt -> Just $ QuitFactionA fid mb toSt fromSt LeadFactionA fid source target -> Just $ LeadFactionA fid target source
Game/LambdaHack/Common/AtomicPos.hs view
@@ -7,6 +7,7 @@ , seenAtomicCli, seenAtomicSer ) where +import Control.Exception.Assert.Sugar import qualified Data.EnumSet as ES import Game.LambdaHack.Common.Action@@ -18,7 +19,6 @@ import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point-import Control.Exception.Assert.Sugar -- All functions here that take an atomic action are executed -- in the state just before the action is executed.@@ -55,65 +55,61 @@ -- contradict state) if the visibility is lower. posCmdAtomic :: MonadActionRO m => CmdAtomic -> m PosAtomic posCmdAtomic cmd = case cmd of- CreateActorA _ body _ ->- return $ PosFidAndSight (bfid body) (blid body) [bpos body]- DestroyActorA _ body _ ->- return $ PosFidAndSight (bfid body) (blid body) [bpos body]+ CreateActorA _ body _ -> posProjBody body+ DestroyActorA _ body _ -> posProjBody body CreateItemA _ _ _ c -> singleContainer c DestroyItemA _ _ _ c -> singleContainer c- SpotActorA _ body _ ->- return $ PosFidAndSight (bfid body) (blid body) [bpos body]- LoseActorA _ body _ ->- return $ PosFidAndSight (bfid body) (blid body) [bpos body]+ SpotActorA _ body _ -> posProjBody body+ LoseActorA _ body _ -> posProjBody body SpotItemA _ _ _ c -> singleContainer c LoseItemA _ _ _ c -> singleContainer c MoveActorA aid fromP toP -> do (lid, _) <- posOfAid aid- return $ PosSight lid [fromP, toP]+ return $! PosSight lid [fromP, toP] WaitActorA aid _ _ -> singleAid aid DisplaceActorA source target -> do (slid, sp) <- posOfAid source (tlid, tp) <- posOfAid target- return $ assert (slid == tlid) $ PosSight slid [sp, tp]+ return $! assert (slid == tlid) $ PosSight slid [sp, tp] MoveItemA _ _ c1 c2 -> do -- works even if moved between positions (lid1, p1) <- posOfContainer c1 (lid2, p2) <- posOfContainer c2- return $ assert (lid1 == lid2) $ PosSight lid1 [p1, p2]+ return $! assert (lid1 == lid2) $ PosSight lid1 [p1, p2] AgeActorA aid _ -> singleAid aid HealActorA aid _ -> singleAid aid HasteActorA aid _ -> singleAid aid- PathActorA aid _ _ -> singleAid aid+ TrajectoryActorA aid _ _ -> singleAid aid ColorActorA aid _ _ -> singleAid aid QuitFactionA{} -> return PosAll- LeadFactionA fid _ _ -> return $ PosFidAndSer fid+ LeadFactionA fid _ _ -> return $! PosFidAndSer fid DiplFactionA{} -> return PosAll- AlterTileA lid p _ _ -> return $ PosSight lid [p]+ AlterTileA lid p _ _ -> return $! PosSight lid [p] SearchTileA aid p _ _ -> do (lid, pos) <- posOfAid aid- return $ PosSight lid [pos, p]+ return $! PosSight lid [pos, p] SpotTileA lid ts -> do let ps = map fst ts- return $ PosSight lid ps+ return $! PosSight lid ps LoseTileA lid ts -> do let ps = map fst ts- return $ PosSight lid ps- AlterSmellA lid p _ _ -> return $ PosSmell lid [p]+ return $! PosSight lid ps+ AlterSmellA lid p _ _ -> return $! PosSmell lid [p] SpotSmellA lid sms -> do let ps = map fst sms- return $ PosSmell lid ps+ return $! PosSmell lid ps LoseSmellA lid sms -> do let ps = map fst sms- return $ PosSmell lid ps- AgeLevelA lid _ -> return $ PosSight lid []+ return $! PosSmell lid ps+ AgeLevelA lid _ -> return $! PosSight lid [] AgeGameA _ -> return PosAll- DiscoverA lid p _ _ -> return $ PosSight lid [p]- CoverA lid p _ _ -> return $ PosSight lid [p]+ DiscoverA lid p _ _ -> return $! PosSight lid [p]+ CoverA lid p _ _ -> return $! PosSight lid [p] PerceptionA{} -> return PosNone- RestartA fid _ _ _ _ _ -> return $ PosFid fid+ RestartA fid _ _ _ _ _ -> return $! PosFid fid RestartServerA _ -> return PosSer- ResumeA fid _ -> return $ PosFid fid+ ResumeA fid _ -> return $! PosFid fid ResumeServerA _ -> return PosSer- KillExitA fid -> return $ PosFid fid+ KillExitA fid -> return $! PosFid fid SaveBkpA -> return PosAll MsgAllA{} -> return PosAll @@ -122,44 +118,50 @@ StrikeD source target _ _ -> do (slid, sp) <- posOfAid source (tlid, tp) <- posOfAid target- return $ assert (slid == tlid) $ PosSight slid [sp, tp]+ return $! assert (slid == tlid) $ PosSight slid [sp, tp] RecoilD source target _ _ -> do (slid, sp) <- posOfAid source (tlid, tp) <- posOfAid target- return $ assert (slid == tlid) $ PosSight slid [sp, tp]+ return $! assert (slid == tlid) $ PosSight slid [sp, tp] ProjectD aid _ -> singleAid aid CatchD aid _ -> singleAid aid ActivateD aid _ -> singleAid aid CheckD aid _ -> singleAid aid TriggerD aid p _ -> do (lid, pa) <- posOfAid aid- return $ PosSight lid [pa, p]+ return $! PosSight lid [pa, p] ShunD aid p _ -> do (lid, pa) <- posOfAid aid- return $ PosSight lid [pa, p]+ return $! PosSight lid [pa, p] EffectD aid _ -> singleAid aid- MsgFidD fid _ -> return $ PosFid fid+ MsgFidD fid _ -> return $! PosFid fid MsgAllD _ -> return PosAll- DisplayPushD fid -> return $ PosFid fid- DisplayDelayD fid -> return $ PosFid fid- RecordHistoryD fid -> return $ PosFid fid+ DisplayPushD fid -> return $! PosFid fid+ DisplayDelayD fid -> return $! PosFid fid+ RecordHistoryD fid -> return $! PosFid fid +posProjBody :: Monad m => Actor -> m PosAtomic+posProjBody body = return $!+ if bproj body+ then PosSight (blid body) [bpos body]+ else PosFidAndSight (bfid body) (blid body) [bpos body]+ singleAid :: MonadActionRO m => ActorId -> m PosAtomic singleAid aid = do b <- getsState $ getActorBody aid- return $ PosFidAndSight (bfid b) (blid b) [bpos b]+ return $! PosSight (blid b) [bpos b] singleContainer :: MonadActionRO m => Container -> m PosAtomic singleContainer c = do (lid, p) <- posOfContainer c- return $ PosSight lid [p]+ return $! PosSight lid [p] -- Determines is a command resets FOV. @Nothing@ means it always does. -- A list of faction means it does for each of the factions. -- This is only an optimization to save perception and spot/lose computation. -- -- Invariant: if @resetsFovAtomic@ determines a faction does not need--- to reset Fov, perception (@perActor@ to be precise, @psmell@ is irrelevant)+-- to reset Fov, perception (@ptotal@ to be precise, @psmell@ is irrelevant) -- of that faction does not change upon recomputation. Otherwise, -- save/restore would change game state. resetsFovAtomic :: MonadActionRO m => CmdAtomic -> m (Maybe [FactionId])@@ -227,10 +229,10 @@ seenAtomicCli :: Bool -> FactionId -> Perception -> PosAtomic -> Bool seenAtomicCli knowEvents fid per posAtomic = case posAtomic of- PosSight _ ps -> knowEvents || all (`ES.member` totalVisible per) ps+ PosSight _ ps -> all (`ES.member` totalVisible per) ps || knowEvents PosFidAndSight fid2 _ ps ->- knowEvents || fid == fid2 || all (`ES.member` totalVisible per) ps- PosSmell _ ps -> knowEvents || all (`ES.member` smellVisible per) ps+ fid == fid2 || all (`ES.member` totalVisible per) ps || knowEvents+ PosSmell _ ps -> all (`ES.member` smellVisible per) ps || knowEvents PosFid fid2 -> fid == fid2 PosFidAndSer fid2 -> fid == fid2 PosSer -> False
Game/LambdaHack/Common/AtomicSem.hs view
@@ -7,11 +7,11 @@ ) where import Control.Arrow (second)+import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM import Data.List -import Control.Exception.Assert.Sugar import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState@@ -23,6 +23,7 @@ import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point+import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time@@ -46,7 +47,7 @@ AgeActorA aid t -> ageActorA aid t HealActorA aid n -> healActorA aid n HasteActorA aid delta -> hasteActorA aid delta- PathActorA aid fromPath toPath -> pathActorA aid fromPath toPath+ TrajectoryActorA aid fromT toT -> trajectoryActorA aid fromT toT ColorActorA aid fromCol toCol -> colorActorA aid fromCol toCol QuitFactionA fid mbody fromSt toSt -> quitFactionA fid mbody fromSt toSt LeadFactionA fid source target -> leadFactionA fid source target@@ -64,8 +65,8 @@ AgeGameA t -> ageGameA t DiscoverA{} -> return () -- Server keeps all atomic comands so the semantics CoverA{} -> return () -- of inverses has to be reasonably inverse.- PerceptionA _ outPA inPA ->- assert (not (EM.null outPA && EM.null inPA)) $ return ()+ PerceptionA _ outPer inPer ->+ assert (not (nullPer outPer && nullPer inPer)) skip RestartA fid sdisco sfper s _ _ -> restartA fid sdisco sfper s RestartServerA s -> restartServerA s ResumeA{} -> return ()@@ -80,7 +81,8 @@ createActorA aid body ais = do -- Add actor to @sactorD@. let f Nothing = Just body- f (Just b) = assert `failure` "actor already added" `twith` (aid, body, b)+ f (Just b) = assert `failure` "actor already added"+ `twith` (aid, body, b) modifyState $ updateActorD $ EM.alter f aid -- Add actor to @sprio@. let g Nothing = Just [aid]@@ -203,7 +205,7 @@ modifyState $ updateActorBody aid $ \body -> body {bpos = toP, boldpos = fromP} -waitActorA :: MonadAction m => ActorId -> Time -> Time -> m ()+waitActorA :: MonadAction m => ActorId -> Bool -> Bool -> m () waitActorA aid fromWait toWait = assert (fromWait /= toWait) $ do b <- getsState $ getActorBody aid assert (fromWait == bwait b `blame` "unexpected waited actor time"@@ -245,11 +247,7 @@ body <- getsState $ getActorBody aid ais <- getsState $ getActorItem aid destroyActorA aid body ais- let newBody = body { btime = timeAdd (btime body) t- , bwait = if bwait body <= btime body- then timeZero -- reset old waiting time- else bwait body -- keep new waiting time- }+ let newBody = body {btime = timeAdd (btime body) t} createActorA aid newBody ais healActorA :: MonadAction m => ActorId -> Int -> m ()@@ -260,17 +258,18 @@ hasteActorA aid delta = assert (delta /= speedZero) $ do modifyState $ updateActorBody aid $ \ b -> let newSpeed = speedAdd (bspeed b) delta- in assert (newSpeed >= speedZero `blame` "actor slowed below zero"- `twith` (aid, delta, bspeed b, newSpeed)) $- b {bspeed = newSpeed}+ in assert (newSpeed >= speedZero+ `blame` "actor slowed below zero"+ `twith` (aid, delta, b, newSpeed))+ $ b {bspeed = newSpeed} -pathActorA :: MonadAction m+trajectoryActorA :: MonadAction m => ActorId -> Maybe [Vector] -> Maybe [Vector] -> m ()-pathActorA aid fromPath toPath = assert (fromPath /= toPath) $ do+trajectoryActorA aid fromT toT = assert (fromT /= toT) $ do body <- getsState $ getActorBody aid- assert (fromPath == bpath body `blame` "unexpected actor path"- `twith` (aid, fromPath, toPath, body)) skip- modifyState $ updateActorBody aid $ \b -> b {bpath = toPath}+ assert (fromT == btrajectory body `blame` "unexpected actor trajectory"+ `twith` (aid, fromT, toT, body)) skip+ modifyState $ updateActorBody aid $ \b -> b {btrajectory = toT} colorActorA :: MonadAction m => ActorId -> Color.Color -> Color.Color -> m ()@@ -319,7 +318,10 @@ modifyState $ updateFaction $ EM.adjust (adj fid1) fid2 -- | Alter an attribute (actually, the only, the defining attribute)--- of a visible tile. This is similar to e.g., @PathActorA@.+-- of a visible tile. This is similar to e.g., @TrajectoryActorA@.+-- We do not modify @lclear@ here, because we can't keep track of+-- alterations to unknown tiles. The server would need to send @lclear@+-- updates for each invisible tile alteration that affects it. alterTileA :: MonadAction m => LevelId -> Point -> Kind.Id TileKind -> Kind.Id TileKind -> m ()@@ -332,18 +334,18 @@ -- and it suddenly changes into another tile, -- which at the same time becomes visible (e.g., an open door). -- See 'AtomicSemCli' for how this is reported to the client.- let adj ts = assert (ts Kind.! p == fromTile- || ts Kind.! p == freshClientTile+ let adj ts = assert (ts PointArray.! p == fromTile+ || ts PointArray.! p == freshClientTile `blame` "unexpected altered tile kind"- `twith` (lid, p, fromTile, toTile, ts Kind.! p))- $ ts Kind.// [(p, toTile)]+ `twith` (lid, p, fromTile, toTile, ts PointArray.! p))+ $ ts PointArray.// [(p, toTile)] updateLevel lid $ updateTile adj case (Tile.isExplorable cotile fromTile, Tile.isExplorable cotile toTile) of (False, True) -> updateLevel lid $ \lvl2 -> lvl2 {lseen = lseen lvl + 1} (True, False) -> updateLevel lid $ \lvl2 -> lvl2 {lseen = lseen lvl - 1} _ -> return () --- Notice a previously invisible tiles. This is similar to @SpotActorA@,+-- Notice previously invisible tiles. This is similar to @SpotActorA@, -- but done in bulk, because it often involves dozens of tiles pers move. -- We don't check that the tiles at the positions in question are unknown -- to save computation, especially for clients that remember tiles@@ -353,17 +355,17 @@ spotTileA lid ts = assert (not $ null ts) $ do Kind.COps{cotile} <- getsState scops Level{ltile} <- getLevel lid- let adj tileMap = tileMap Kind.// ts+ let adj tileMap = tileMap PointArray.// ts updateLevel lid $ updateTile adj let f (p, t2) = do- let t1 = ltile Kind.! p+ let t1 = ltile PointArray.! p case (Tile.isExplorable cotile t1, Tile.isExplorable cotile t2) of (False, True) -> updateLevel lid $ \lvl -> lvl {lseen = lseen lvl+1} (True, False) -> updateLevel lid $ \lvl -> lvl {lseen = lseen lvl-1} _ -> return () mapM_ f ts --- Stop noticing a previously visible tiles. Unlike @spotTileA@, it verifies+-- Stop noticing previously visible tiles. Unlike @spotTileA@, it verifies -- the state of the tiles before changing them. loseTileA :: MonadAction m => LevelId -> [(Point, Kind.Id TileKind)] -> m () loseTileA lid ts = assert (not $ null ts) $ do@@ -371,9 +373,9 @@ let unknownId = ouniqGroup "unknown space" matches _ [] = True matches tileMap ((p, ov) : rest) =- tileMap Kind.! p == ov && matches tileMap rest+ tileMap PointArray.! p == ov && matches tileMap rest tu = map (second (const unknownId)) ts- adj tileMap = assert (matches tileMap ts) $ tileMap Kind.// tu+ adj tileMap = assert (matches tileMap ts) $ tileMap PointArray.// tu updateLevel lid $ updateTile adj let f (_, t1) = when (Tile.isExplorable cotile t1) $@@ -382,21 +384,16 @@ alterSmellA :: MonadAction m => LevelId -> Point -> Maybe Time -> Maybe Time -> m ()-alterSmellA lid p _fromSm toSm = do- -- TODO: this rarely crashes when a dominated smelling monster exists:- -- let alt sm = assert (sm == fromSm `blame` "unexpected tile smell"- -- `twith` (lid, p, fromSm, toSm, sm)) toSm- let alt _ = toSm+alterSmellA lid p fromSm toSm = do+ let alt sm = assert (sm == fromSm `blame` "unexpected tile smell"+ `twith` (lid, p, fromSm, toSm, sm)) toSm updateLevel lid $ updateSmell $ EM.alter alt p spotSmellA :: MonadAction m => LevelId -> [(Point, Time)] -> m () spotSmellA lid sms = assert (not $ null sms) $ do let alt sm Nothing = Just sm- alt sm (Just _) = Just sm--- TODO: a hack to sidestep server not disabling the nose of fresh actors,--- see smellFromActors--- alt sm (Just oldSm) = assert `failure` "smell already added"--- `twith` (lid, sms, sm, oldSm)+ alt sm (Just oldSm) = assert `failure` "smell already added"+ `twith` (lid, sms, sm, oldSm) f (p, sm) = EM.alter (alt sm) p upd m = foldr f m sms updateLevel lid $ updateSmell upd
Game/LambdaHack/Common/ClientCmd.hs view
@@ -47,7 +47,7 @@ CmdAtomicAI cmdA@SpotTileA{} -> debugPlain cmd cmdA CmdAtomicAI cmdA -> debugPretty cmd cmdA CmdQueryAI aid -> debugAid aid "CmdQueryAI" cmd- CmdPingAI -> return $ showT cmd+ CmdPingAI -> return $! tshow cmd debugCmdClientUI :: MonadActionRO m => CmdClientUI -> m Text debugCmdClientUI cmd = case cmd of@@ -57,19 +57,19 @@ CmdAtomicUI cmdA -> debugPretty cmd cmdA SfxAtomicUI sfx -> do ps <- posSfxAtomic sfx- return $ showT (cmd, ps)+ return $! tshow (cmd, ps) CmdQueryUI aid -> debugAid aid "CmdQueryUI" cmd- CmdPingUI -> return $ showT cmd+ CmdPingUI -> return $! tshow cmd debugPretty :: (MonadActionRO m, Show a) => a -> CmdAtomic -> m Text debugPretty cmd cmdA = do ps <- posCmdAtomic cmdA- return $ showT (cmd, ps)+ return $! tshow (cmd, ps) debugPlain :: (MonadActionRO m, Show a) => a -> CmdAtomic -> m Text debugPlain cmd cmdA = do ps <- posCmdAtomic cmdA- return $ T.pack $ show (cmd, ps) -- too large for pretty show+ return $! T.pack $ show (cmd, ps) -- too large for pretty show data DebugAid a = DebugAid { label :: !Text@@ -88,12 +88,12 @@ else do b <- getsState $ getActorBody aid time <- getsState $ getLocalTime (blid b)- return $ showT DebugAid { label- , cmd- , lid = blid b- , time- , aid- , faction = bfid b }+ return $! tshow DebugAid { label+ , cmd+ , lid = blid b+ , time+ , aid+ , faction = bfid b } -- | Connection channels between the server and a single client. data ChanServer c d = ChanServer@@ -104,7 +104,7 @@ -- | Connections to the human-controlled client of a faction and -- to the AI client for the same faction. type ConnServerFaction = ( Maybe (ChanFrontend, ChanServer CmdClientUI CmdSer)- , ChanServer CmdClientAI CmdSerTakeTime )+ , ChanServer CmdClientAI CmdTakeTimeSer ) -- | Connection information for all factions, indexed by faction identifier. type ConnServerDict = EM.EnumMap FactionId ConnServerFaction
Game/LambdaHack/Common/Color.hs view
@@ -9,6 +9,7 @@ ) where import Data.Binary+import Data.Bits (unsafeShiftL, unsafeShiftR, (.&.)) import qualified Data.Hashable as Hashable import GHC.Generics (Generic) @@ -39,10 +40,6 @@ instance Hashable.Hashable Color -instance Binary Color where- put = putWord8 . toEnum . fromEnum- get = fmap (toEnum . fromEnum) getWord8- -- | The default colours, to optimize attribute setting. defBG, defFG :: Color defBG = Black@@ -55,14 +52,10 @@ } deriving (Show, Eq, Ord) -instance Binary Attr where- put Attr{..} = do- put fg- put bg- get = do- fg <- get- bg <- get- return Attr{..}+instance Enum Attr where+ fromEnum Attr{..} = fromEnum fg + unsafeShiftL (fromEnum bg) 8+ toEnum n = Attr (toEnum $ n .&. (2 ^ (8 :: Int) - 1))+ (toEnum $ unsafeShiftR n 8) -- | The default attribute, to optimize attribute setting. defAttr :: Attr@@ -72,16 +65,12 @@ { acAttr :: !Attr , acChar :: !Char }- deriving (Show, Eq)+ deriving (Show, Eq, Ord) -instance Binary AttrChar where- put AttrChar{..} = do- put acAttr- put acChar- get = do- acAttr <- get- acChar <- get- return AttrChar{..}+instance Enum AttrChar where+ fromEnum AttrChar{..} = fromEnum acAttr + unsafeShiftL (fromEnum acChar) 16+ toEnum n = AttrChar (toEnum $ n .&. (2 ^ (16 :: Int) - 1))+ (toEnum $ unsafeShiftR n 16) -- | A helper for the terminal frontends that display bright via bold. isBright :: Color -> Bool@@ -136,3 +125,25 @@ _olorToRGB BrMagenta = "#FF55FF" _olorToRGB BrCyan = "#55FFFF" _olorToRGB BrWhite = "#FFFFFF"++instance Binary Color where+ put = putWord8 . toEnum . fromEnum+ get = fmap (toEnum . fromEnum) getWord8++instance Binary Attr where+ put Attr{..} = do+ put fg+ put bg+ get = do+ fg <- get+ bg <- get+ return $! Attr{..}++instance Binary AttrChar where+ put AttrChar{..} = do+ put acAttr+ put acChar+ get = do+ acAttr <- get+ acChar <- get+ return $! AttrChar{..}
− Game/LambdaHack/Common/ConfigIO.hs
@@ -1,97 +0,0 @@--- | Personal game configuration file support.-module Game.LambdaHack.Common.ConfigIO- ( CP, appDataDir, mkConfig, get, getOption, set, getItems, to_string, dump- ) where--import Control.Exception.Assert.Sugar-import qualified Data.Char as Char-import qualified Data.ConfigFile as CF-import System.Directory-import System.Environment-import System.FilePath--import Game.LambdaHack.Server.Config--overrideCP :: CP -> FilePath -> IO CP-overrideCP cp@(CP defCF) cfile = do- cpExists <- doesFileExist cfile- if not cpExists- then return cp- else do- c <- CF.readfile defCF cfile- return $ toCP $ assert `forceEither` c---- | 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 TH.--- 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 = assert `forceEither` CF.readstring CF.emptyCP unConfig- !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 = takeWhile Char.isAlphaNum progName- getAppUserDataDirectory name---- | Dumps the current configuration to a file.-dump :: Config -> FilePath -> IO ()-dump Config{configSelfString} fn = do- current <- getCurrentDirectory- 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-set (CP conf) s o v =- if CF.has_option conf s o- then assert `failure`"overwritten config option" `twith` (s, o)- else CP $ assert `forceEither` CF.set conf s o v---- | 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}---- | 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 $ assert `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 assert `forceEither` CF.get conf s o- else assert `failure` "unknown CF option" `twith` (s, o, CF.to_string conf)---- | 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 assert `forceEither` CF.items conf s- else assert `failure` "unknown CF section" `twith` (s, CF.to_string conf)--to_string :: CP -> String-to_string (CP conf) = CF.to_string conf
Game/LambdaHack/Common/Effect.hs view
@@ -5,6 +5,7 @@ ( Effect(..), effectTrav, effectToSuffix ) where +import Control.Exception.Assert.Sugar import qualified Control.Monad.State as St import Data.Binary import qualified Data.Hashable as Hashable@@ -13,7 +14,6 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Random-import Control.Exception.Assert.Sugar -- TODO: document each constructor -- Effects of items, tiles, etc. The type argument represents power.@@ -22,7 +22,7 @@ NoEffect | Heal !Int | Hurt !RollDice !a- | Mindprobe !Int -- the @Int@ is a hack to send the result to clients+ | Mindprobe Int -- the @Int@ is a lazy hack to send the result to clients | Dominate | CallFriend !Int | Summon !Int@@ -31,7 +31,7 @@ | Regeneration !a | Searching !a | Ascend !Int- | Escape+ | Escape !Int deriving (Show, Read, Eq, Ord, Generic, Functor) instance Hashable.Hashable a => Hashable.Hashable (Effect a)@@ -42,24 +42,24 @@ -- | Transform an effect using a stateful function. effectTrav :: Effect a -> (a -> St.State s b) -> St.State s (Effect b) effectTrav NoEffect _ = return NoEffect-effectTrav (Heal p) _ = return $ Heal p+effectTrav (Heal p) _ = return $! Heal p effectTrav (Hurt dice a) f = do b <- f a- return $ Hurt dice b-effectTrav (Mindprobe x) _ = return $ Mindprobe x+ return $! Hurt dice b+effectTrav (Mindprobe x) _ = return $! Mindprobe x effectTrav Dominate _ = return Dominate-effectTrav (CallFriend p) _ = return $ CallFriend p-effectTrav (Summon p) _ = return $ Summon p-effectTrav (CreateItem p) _ = return $ CreateItem p+effectTrav (CallFriend p) _ = return $! CallFriend p+effectTrav (Summon p) _ = return $! Summon p+effectTrav (CreateItem p) _ = return $! CreateItem p effectTrav ApplyPerfume _ = return ApplyPerfume effectTrav (Regeneration a) f = do b <- f a- return $ Regeneration b+ return $! Regeneration b effectTrav (Searching a) f = do b <- f a- return $ Searching b-effectTrav (Ascend p) _ = return $ Ascend p-effectTrav Escape _ = return Escape+ return $! Searching b+effectTrav (Ascend p) _ = return $! Ascend p+effectTrav (Escape p) _ = return $! Escape p -- | Suffix to append to a basic content name if the content causes the effect. effectToSuff :: Show a => Effect a -> (a -> Text) -> Text@@ -69,7 +69,7 @@ Heal p | p > 0 -> "of healing" <> affixBonus p Heal 0 -> "of bloodletting" Heal p -> "of wounding" <> affixBonus p- Hurt dice t -> "(" <> showT dice <> ")" <> t+ Hurt dice t -> "(" <> tshow dice <> ")" <> t Mindprobe{} -> "of soul searching" Dominate -> "of domination" CallFriend p -> "of aid calling" <> affixPower p@@ -81,7 +81,7 @@ Ascend p | p > 0 -> "of ascending" <> affixPower p Ascend p | p < 0 -> "of descending" <> affixPower (- p) Ascend{} -> assert `failure` effect- Escape -> "of escaping"+ Escape{} -> "of escaping" effectToSuffix :: Effect Int -> Text effectToSuffix effect = effectToSuff effect affixBonus@@ -90,10 +90,10 @@ affixPower p = case compare p 1 of EQ -> "" LT -> assert `failure` "power less than 1" `twith` p- GT -> " (+" <> showT p <> ")"+ GT -> " (+" <> tshow p <> ")" affixBonus :: Int -> Text affixBonus p = case compare p 0 of EQ -> ""- LT -> " (" <> showT p <> ")"- GT -> " (+" <> showT p <> ")"+ LT -> " (" <> tshow p <> ")"+ GT -> " (+" <> tshow p <> ")"
Game/LambdaHack/Common/Faction.hs view
@@ -2,7 +2,7 @@ -- the hero faction battling the monster and the animal factions. module Game.LambdaHack.Common.Faction ( FactionId, FactionDict, Faction(..), Diplomacy(..), Outcome(..), Status(..)- , isSpawnFact, isSummonFact, isAtWar, isAllied+ , isHeroFact, isHorrorFact, isSpawnFact, isSummonFact, isAtWar, isAllied ) where import Data.Binary@@ -58,12 +58,22 @@ } deriving (Show, Eq, Ord) --- | Tell whether the faction can spawn actors.-isSpawnFact :: Kind.COps -> Faction -> Bool-isSpawnFact Kind.COps{cofaction=Kind.Ops{okind}} fact =+-- | Tell whether the faction consists of heroes.+isHeroFact :: Kind.COps -> Faction -> Bool+isHeroFact Kind.COps{cofaction=Kind.Ops{okind}} fact = let kind = okind (gkind fact)- in maybe False (> 0) $ lookup "spawn" $ ffreq kind+ in maybe False (> 0) $ lookup "hero" $ ffreq kind +-- | Tell whether the faction consists of summoned horrors only.+isHorrorFact :: Kind.COps -> Faction -> Bool+isHorrorFact Kind.COps{cofaction=Kind.Ops{okind}} fact =+ let kind = okind (gkind fact)+ in maybe False (> 0) $ lookup "horror" $ ffreq kind++-- | Tell whether the faction can spawn actors.+isSpawnFact :: Faction -> Bool+isSpawnFact fact = playerSpawn (gplayer fact) > 0+ -- | Tell whether actors of the faction can be summoned by items, etc. isSummonFact :: Kind.COps -> Faction -> Bool isSummonFact Kind.COps{cofaction=Kind.Ops{okind}} fact =@@ -95,7 +105,7 @@ gdipl <- get gquit <- get gleader <- get- return Faction{..}+ return $! Faction{..} instance Binary Diplomacy where put Unknown = putWord8 0@@ -138,4 +148,4 @@ stOutcome <- get stDepth <- get stInfo <- get- return Status{..}+ return $! Status{..}
Game/LambdaHack/Common/Feature.hs view
@@ -10,27 +10,25 @@ import Game.LambdaHack.Common.Effect --- | All possible terrain tile features, some of them parameterized--- or dependent on outside coefficients, e.g., on the tile secrecy value.+-- | All possible terrain tile features. data Feature = Cause !(Effect Int) -- ^ causes the effect when triggered- | OpenTo !Text -- ^ goes from an open to a closed tile when altered- | CloseTo !Text -- ^ goes from a closed to an open tile when altered+ | OpenTo !Text -- ^ goes from a closed to an open tile when altered+ | CloseTo !Text -- ^ goes from an open to a closed tile when altered | ChangeTo !Text -- ^ alters tile, but does not change walkability | HideAs !Text -- ^ when hidden, looks as a tile of the group | RevealAs !Text -- ^ if secret, can be revealed to belong to the group | Walkable -- ^ actors can walk through | Clear -- ^ actors can see through- | Lit -- ^ is lit with an ambient shine+ | Dark -- ^ is not lit with an ambient shine | Suspect -- ^ may not be what it seems (clients only) | Aura !(Effect Int) -- ^ sustains the effect continuously, TODO | Impenetrable -- ^ can never be excavated nor seen through | CanItem -- ^ items can be generated there | CanActor -- ^ actors and stairs can be generated there- | Exit -- ^ is a (not hidden) door, stair, etc.- | Path -- ^ used for visible paths throughout the level+ | Trail -- ^ used for visible trails throughout the level deriving (Show, Read, Eq, Ord, Generic) instance Binary Feature
Game/LambdaHack/Common/Flavour.hs view
@@ -13,7 +13,6 @@ import Data.Binary import qualified Data.Hashable as Hashable-import qualified Data.List as L import Data.Text (Text) import GHC.Generics (Generic) @@ -33,8 +32,8 @@ -- | Turn a colour set into a flavour set. zipPlain, zipFancy :: [Color] -> [Flavour]-zipPlain = L.map (Flavour False)-zipFancy = L.map (Flavour True)+zipPlain = map (Flavour False)+zipFancy = map (Flavour True) -- | The standard full set of flavours. stdFlav :: [Flavour]
Game/LambdaHack/Common/HighScore.hs view
@@ -1,11 +1,10 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | High score table operations. module Game.LambdaHack.Common.HighScore- ( ScoreTable, empty, register, slideshow+ ( ScoreTable, empty, register, highSlideshow ) where 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@@ -20,36 +19,39 @@ -- | A single score record. Records are ordered in the highscore table, -- from the best to the worst, in lexicographic ordering wrt the fields below. data ScoreRecord = ScoreRecord- { points :: !Int -- ^ the score- , negTime :: !Time -- ^ game time spent (negated, so less better)- , date :: !ClockTime -- ^ date of the last game interruption- , status :: !Status -- ^ reason of the game interruption+ { points :: !Int -- ^ the score+ , negTime :: !Time -- ^ game time spent (negated, so less better)+ , date :: !ClockTime -- ^ date of the last game interruption+ , status :: !Status -- ^ reason of the game interruption+ , difficulty :: !Int -- ^ difficulty of the game } deriving (Eq, Ord) +-- TODO: move all to Text -- | Show a single high score, from the given ranking in the high score table. showScore :: (Int, ScoreRecord) -> [Text] showScore (pos, score) = let Status{stOutcome, stDepth} = status score died = case stOutcome of- Killed -> "perished on level " ++ show (abs stDepth) ++ ","- Defeated -> "was defeated"- Camping -> "is camping somewhere,"- Conquer -> "eliminated all opposition"- Escape -> "emerged victorious"- Restart -> "resigned prematurely"+ Killed -> "Perished on level " ++ show (abs stDepth)+ Defeated -> "Was defeated"+ Camping -> "Camps somewhere"+ Conquer -> "Slew all opposition"+ Escape -> "Emerged victorious"+ Restart -> "Resigned prematurely" curDate = calendarTimeToString . toUTCTime . date $ score- big, lil :: String- big = " "- lil = " " turns = - (negTime score `timeFit` timeTurn)+ diff = 5 - difficulty score+ diffText :: String+ diffText | diff == 5 = ""+ | otherwise = printf " (difficulty %d)" diff -- TODO: the spaces at the end are hand-crafted. Remove when display -- of overlays adds such spaces automatically. 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+ [ ""+ , printf "%4d. %6d %s%s"+ pos (points score) died diffText+ , " " ++ printf "after %d turns on %s." turns curDate ] -- | The list of scores, in decreasing order.@@ -67,8 +69,8 @@ -- Make sure the table doesn't grow too large. insertPos :: ScoreRecord -> ScoreTable -> (ScoreTable, Int) insertPos s (ScoreTable table) =- let (prefix, suffix) = L.span (> s) table- pos = L.length prefix + 1+ let (prefix, suffix) = span (> s) table+ pos = length prefix + 1 in (ScoreTable $ prefix ++ [s] ++ take (100 - pos) suffix, pos) -- | Register a new score in a score table.@@ -77,42 +79,45 @@ -> Time -- ^ game time spent -> Status -- ^ reason of the game interruption -> ClockTime -- ^ current date+ -> Int -- ^ difficulty level -> Maybe (ScoreTable, Int)-register table total time status@Status{stOutcome} date =- let points = if stOutcome `elem` [Killed, Defeated, Restart]- then (total + 1) `div` 2- else if stOutcome == Conquer- then let turnsSpent = timeFit time timeTurn- speedup = 10000 - 5 * turnsSpent- bonus = sqrt $ fromIntegral speedup :: Double- in 10 + floor bonus- else total+register table total time status@Status{stOutcome} date difficulty =+ let pUnscaled = if stOutcome `elem` [Killed, Defeated, Restart]+ then (total + 1) `div` 2+ else if stOutcome == Conquer+ then let turnsSpent = timeFit time timeTurn+ speedup = 10000 - 5 * turnsSpent+ bonus = sqrt $ fromIntegral speedup :: Double+ in 10 + floor bonus+ else total+ points = (round :: Double -> Int)+ $ fromIntegral pUnscaled * 1.5 ^^ difficulty negTime = timeNegate time score = ScoreRecord{..} in if points > 0 then Just $ insertPos score table else Nothing -- | Show a screenful of the high scores table. -- Parameter height is the number of (3-line) scores to be shown.-showTable :: ScoreTable -> Int -> Int -> Overlay-showTable (ScoreTable table) start height =+tshowable :: ScoreTable -> Int -> Int -> [Text]+tshowable (ScoreTable table) start height = let zipped = zip [1..] table screenful = take height . drop (start - 1) $ zipped in concatMap showScore screenful ++ [moreMsg] -- | Produce a couple of renderings of the high scores table.-showCloseScores :: Int -> ScoreTable -> Int -> [Overlay]+showCloseScores :: Int -> ScoreTable -> Int -> [[Text]] showCloseScores pos h height = if pos <= height- then [showTable h 1 height]- else [showTable h 1 height,- showTable h (max (height + 1) (pos - height `div` 2)) height]+ then [tshowable h 1 height]+ else [tshowable h 1 height,+ tshowable h (max (height + 1) (pos - height `div` 2)) height] -- | Generate a slideshow with the current and previous scores.-slideshow :: ScoreTable -- ^ current score table- -> Int -- ^ position of the current high score in the table- -> Status -- ^ reason of the game interruption- -> Slideshow-slideshow table pos status =+highSlideshow :: ScoreTable -- ^ current score table+ -> Int -- ^ position of the current score in the table+ -> Status -- ^ reason of the game interruption+ -> Slideshow+highSlideshow table pos status = let (_, nlines) = normalLevelBound -- TODO: query terminal size instead height = nlines `div` 3 (subject, person, msgUnless) =@@ -141,19 +146,21 @@ [ MU.SubjectVerb person MU.Yes subject "award you" , MU.Ordinal pos, "place" , msgUnless ]- in toSlideshow $ map ([msg] ++) $ showCloseScores pos table height+ in toSlideshow True $ map ([msg] ++) $ showCloseScores pos table height instance Binary ScoreRecord where- put (ScoreRecord p n (TOD cs cp) s) = do+ put (ScoreRecord p n (TOD cs cp) s difficulty) = do put p put n put cs put cp put s+ put difficulty get = do p <- get n <- get cs <- get cp <- get s <- get- return (ScoreRecord p n (TOD cs cp) s)+ difficulty <- get+ return $! ScoreRecord p n (TOD cs cp) s difficulty
+ Game/LambdaHack/Common/HumanCmd.hs view
@@ -0,0 +1,158 @@+-- | Abstract syntax human player commands.+module Game.LambdaHack.Common.HumanCmd+ ( CmdCategory(..), HumanCmd(..), Trigger(..)+ , noRemoteHumanCmd, categoryDescription, cmdDescription+ ) where++import Control.Exception.Assert.Sugar+import Data.Text (Text)+import qualified NLP.Miniutter.English as MU++import qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Vector++data CmdCategory = CmdMenu | CmdMove | CmdItem | CmdTgt | CmdMeta | CmdDebug+ deriving (Show, Read, Eq)++categoryDescription :: CmdCategory -> Text+categoryDescription CmdMenu = "Main Menu"+categoryDescription CmdMove = "Movement and terrain alteration"+categoryDescription CmdItem = "Inventory and items"+categoryDescription CmdTgt = "Targeting"+categoryDescription CmdMeta = "Assorted"+categoryDescription CmdDebug = "Debug"++-- | Abstract syntax of player commands.+data HumanCmd =+ -- These usually take time.+ Move !Vector+ | Run !Vector+ | Wait+ | Pickup+ | Drop+ | Project ![Trigger]+ | Apply ![Trigger]+ | AlterDir ![Trigger]+ | TriggerTile ![Trigger]+ | StepToTarget+ | Resend+ -- Below this line, commands do not take time.+ | GameRestart !Text+ | GameExit+ | GameSave+ | GameDifficultyCycle+ -- Below this line, commands do not notify the server.+ | PickLeader !Int+ | MemberCycle+ | MemberBack+ | Inventory+ | SelectActor+ | SelectNone+ | Clear+ | Repeat !Int+ | Record+ | History+ | MarkVision+ | MarkSmell+ | MarkSuspect+ | Help+ | MainMenu+ | Macro !Text ![String]+ -- These are mostly related to targeting.+ | MoveCursor !Vector !Int+ | TgtFloor+ | TgtEnemy+ | TgtUnknown+ | TgtItem+ | TgtStair !Bool+ | TgtAscend !Int+ | EpsIncr !Bool+ | TgtClear+ | Cancel+ | Accept+ deriving (Eq, Ord, Show, Read)++data Trigger =+ ApplyItem {verb :: !MU.Part, object :: !MU.Part, symbol :: !Char}+ | AlterFeature {verb :: !MU.Part, object :: !MU.Part, feature :: !F.Feature}+ | TriggerFeature {verb :: !MU.Part, object :: !MU.Part, feature :: !F.Feature}+ deriving (Show, Read, Eq, Ord)++-- | Commands that are forbidden on a remote level, because they+-- would usually take time when invoked on one.+-- Note that some commands that take time are not included,+-- because they don't take time in targeting mode.+noRemoteHumanCmd :: HumanCmd -> Bool+noRemoteHumanCmd cmd = case cmd of+ Wait -> True+ Pickup -> True+ Drop -> True+ Project{} -> True+ Apply{} -> True+ AlterDir{} -> True+ StepToTarget -> True+ Resend -> True+ _ -> False++-- | Description of player commands.+cmdDescription :: HumanCmd -> Text+cmdDescription cmd = case cmd of+ Move v -> "move" <+> compassText v+ Run v -> "run" <+> compassText v+ Wait -> "wait"+ Pickup -> "get an object"+ Drop -> "drop an object"+ Project ts -> triggerDescription ts+ Apply ts -> triggerDescription ts+ AlterDir ts -> triggerDescription ts+ TriggerTile ts -> triggerDescription ts+ StepToTarget -> "make one step towards the target"+ Resend -> "resend last server command"++ GameRestart t -> "new" <+> t <+> "game"+ GameExit -> "save and exit"+ GameSave -> "save game"++ GameDifficultyCycle -> "cycle difficulty of the next game"+ PickLeader{} -> "pick leader"+ MemberCycle -> "cycle among party members on the level"+ MemberBack -> "cycle among party members in the dungeon"+ Inventory -> "display inventory"+ SelectActor -> "select (or deselect) a party member"+ SelectNone -> "deselect (or select) all on the level"+ Clear -> "clear messages"+ Repeat 1 -> "play back last keys"+ Repeat n -> "play back last keys" <+> tshow n <+> "times"+ Record -> "start recording a macro"+ History -> "display player diary"+ MarkVision -> "mark visible area"+ MarkSmell -> "mark smell"+ MarkSuspect -> "mark suspect terrain"+ Help -> "display help"+ MainMenu -> "display the Main Menu"+ Macro t _ -> t++ MoveCursor v 1 -> "move cursor" <+> compassText v+ MoveCursor v k ->+ "move cursor up to" <+> tshow k <+> "steps" <+> compassText v+ TgtFloor -> "cycle targeting mode"+ TgtEnemy -> "target enemy"+ TgtUnknown -> "target the closest unknown spot"+ TgtItem -> "target the closest item"+ TgtStair up -> "target the closest stairs" <+> if up then "up" else "down"+ TgtAscend k | k == 1 -> "target next shallower level"+ TgtAscend k | k >= 2 -> "target" <+> tshow k <+> "levels shallower"+ TgtAscend k | k == -1 -> "target next deeper level"+ TgtAscend k | k <= -2 -> "target" <+> tshow (-k) <+> "levels deeper"+ TgtAscend _ -> assert `failure` "void level change when targeting"+ `twith` cmd+ EpsIncr True -> "swerve targeting line"+ EpsIncr False -> "unswerve targeting line"+ TgtClear -> "clear target/cursor"+ Cancel -> "cancel action"+ Accept -> "accept choice"++triggerDescription :: [Trigger] -> Text+triggerDescription [] = "trigger a thing"+triggerDescription (t : _) = makePhrase [verb t, object t]
Game/LambdaHack/Common/Item.hs view
@@ -6,7 +6,7 @@ -- inventory manangement and items proper. module Game.LambdaHack.Common.Item ( -- * Teh @Item@ type- ItemId, Item(..), jkind, buildItem, newItem, viewItem+ ItemId, Item(..), jkind, buildItem, newItem -- * Inventory search , strongestSearch, strongestSword, strongestRegen -- * The item discovery types@@ -15,8 +15,11 @@ , FlavourMap, emptyFlavourMap, dungeonFlavourMap -- * Textual description , partItem, partItemWs, partItemAW+ -- * Assorted+ , isFragile, isExplosive, isLingering, causeIEffects ) where +import Control.Exception.Assert.Sugar import Control.Monad import Data.Binary import qualified Data.EnumMap.Strict as EM@@ -29,15 +32,14 @@ import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU -import qualified Game.LambdaHack.Common.Color as Color import Game.LambdaHack.Common.Effect import Game.LambdaHack.Common.Flavour+import qualified Game.LambdaHack.Common.ItemFeature as IF import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Random import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Content.RuleKind-import Control.Exception.Assert.Sugar import Game.LambdaHack.Utils.Frequency -- | A unique identifier of an item in the dungeon.@@ -67,6 +69,7 @@ , jname :: !Text -- ^ individual generic name , jflavour :: !Flavour -- ^ individual flavour , jeffect :: !(Effect Int) -- ^ the effect when activated+ , jweight :: !Int -- ^ weight in grams, obvious enough } deriving (Show, Eq, Ord, Generic) @@ -106,20 +109,21 @@ case iflavour kind of [fl] -> fl _ -> flavour EM.! ikChosen+ jweight = iweight kind in Item{..} -- | Generate an item based on level. newItem :: Kind.Ops ItemKind -> FlavourMap -> DiscoRev -> Frequency Text -> Int -> Int -> Rnd (Item, Int, ItemKind)-newItem cops@Kind.Ops{opick, okind} flavour discoRev itemFreq lvl depth = do+newItem coitem@Kind.Ops{opick, okind} flavour discoRev itemFreq lvl depth = do itemGroup <- frequency itemFreq let castItem :: Int -> Rnd (Item, Int, ItemKind) castItem 0 | nullFreq itemFreq = assert `failure` "no fallback items" `twith` (itemFreq, lvl, depth) castItem 0 = do let newFreq = setFreq itemFreq itemGroup 0- newItem cops flavour discoRev newFreq lvl depth+ newItem coitem flavour discoRev newFreq lvl depth castItem count = do ikChosen <- fmap (fromMaybe $ assert `failure` itemGroup) $ opick itemGroup (const True)@@ -128,16 +132,15 @@ if jcount == 0 then castItem $ count - 1 else do- effect <- effectTrav (ieffect kind) (castDeep lvl depth)+ let kindEffect = case causeIEffects coitem ikChosen of+ [] -> NoEffect+ eff : _TODO -> eff+ effect <- effectTrav kindEffect (castDeep lvl depth) return ( buildItem flavour discoRev ikChosen kind effect , jcount , kind ) castItem 10 --- | Represent an item on the map.-viewItem :: Item -> (Char, Color.Color)-viewItem i = (jsymbol i, flavourToColor $ jflavour i)- -- | Flavours assigned by the server to item kinds, in this particular game. newtype FlavourMap = FlavourMap (EM.EnumMap (Kind.Id ItemKind) Flavour) deriving (Show, Binary)@@ -193,6 +196,40 @@ strongestRegen is = strongestItem is $ \ i -> case jeffect i of Regeneration k -> Just k; _ -> Nothing++isFragile :: Kind.Ops ItemKind -> Discovery -> Item -> Bool+isFragile Kind.Ops{okind} disco i =+ case jkind disco i of+ Nothing -> False+ Just ik ->+ let getTo IF.Fragile _acc = True+ getTo IF.Explode{} _acc = True+ getTo _ acc = acc+ in foldr getTo False $ ifeature $ okind ik++isExplosive :: Kind.Ops ItemKind -> Discovery -> Item -> Maybe Text+isExplosive Kind.Ops{okind} disco i =+ case jkind disco i of+ Nothing -> Nothing+ Just ik ->+ let getTo (IF.Explode cgroup) _acc = Just cgroup+ getTo _ acc = acc+ in foldr getTo Nothing $ ifeature $ okind ik++isLingering :: Kind.Ops ItemKind -> Discovery -> Item -> Int+isLingering Kind.Ops{okind} disco i =+ case jkind disco i of+ Nothing -> 100+ Just ik ->+ let getTo (IF.Linger percent) _acc = percent+ getTo _ acc = acc+ in foldr getTo 100 $ ifeature $ okind ik++causeIEffects :: Kind.Ops ItemKind -> Kind.Id ItemKind -> [Effect RollDeep]+causeIEffects Kind.Ops{okind} ik = do+ let getTo (IF.Cause eff) acc = eff : acc+ getTo _ acc = acc+ foldr getTo [] $ ifeature $ okind ik -- | The part of speech describing the item. partItem :: Kind.Ops ItemKind -> Discovery -> Item -> (MU.Part, MU.Part)
+ Game/LambdaHack/Common/ItemFeature.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE DeriveGeneric #-}+-- | Item features.+module Game.LambdaHack.Common.ItemFeature+ ( Feature(..)+ ) where++import Data.Binary+import Data.Text (Text)+import GHC.Generics (Generic)++import Game.LambdaHack.Common.Effect+import Game.LambdaHack.Common.Random++-- | All possible item features.+data Feature =+ Cause !(Effect RollDeep) -- ^ causes the effect when triggered+ | ChangeTo !Text -- ^ changes to this item kind group when altered+ | Explode !Text -- ^ explode, producing this group of shrapnel+ | Fragile -- ^ breaks even when not hitting an enemy+ | Linger !Int -- ^ fly for this percentage of 2 turns+ deriving (Show, Eq, Ord, Generic)++instance Binary Feature
Game/LambdaHack/Common/Key.hs view
@@ -1,20 +1,20 @@+{-# LANGUAGE DeriveGeneric #-} -- | Frontend-independent keyboard input operations. module Game.LambdaHack.Common.Key ( Key(..), handleDir, dirAllMoveKey- , moveBinding, keyTranslate, Modifier(..), KM(..), showKM, escKey+ , moveBinding, mkKM, keyTranslate, Modifier(..), KM(..), showKM, escKey ) where +import Control.Exception.Assert.Sugar import Data.Binary import qualified Data.Char as Char-import qualified Data.List as L import Data.Text (Text) import qualified Data.Text as T+import GHC.Generics (Generic) import Prelude hiding (Left, Right) import Game.LambdaHack.Common.Msg-import Game.LambdaHack.Common.PointXY import Game.LambdaHack.Common.Vector-import Game.LambdaHack.Common.VectorXY -- TODO: if the file grows much larger, split it and move a part to Utils/ @@ -25,6 +25,7 @@ | Space | Tab | BackTab+ | BackSpace | PgUp | PgDn | Left@@ -37,78 +38,25 @@ | KP !Char -- ^ a keypad key for a character (digits and operators) | Char !Char -- ^ a single printable character | Unknown !Text -- ^ an unknown key, registered to warn the user- deriving (Ord, Eq)+ deriving (Read, Ord, Eq, Generic) -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)"+instance Binary Key -- | Our own encoding of modifiers. Incomplete. data Modifier = NoModifier | Control- deriving (Ord, Eq)+ deriving (Read, Ord, Eq, Generic) -instance Binary Modifier where- put NoModifier = putWord8 0- put Control = putWord8 1- get = do- tag <- getWord8- case tag of- 0 -> return NoModifier- 1 -> return Control- _ -> fail "no parse (Modifier)"+instance Binary Modifier data KM = KM {modifier :: !Modifier, key :: !Key}- deriving (Ord, Eq)+ deriving (Read, Ord, Eq, Generic) instance Show KM where show = T.unpack . showKM -instance Binary KM where- put KM {..} = do- put key- put modifier- get = do- key <- get- modifier <- get- return KM {..}+instance Binary KM -- Common and terse names for keys. showKey :: Key -> Text@@ -118,15 +66,16 @@ showKey Space = "SPACE" showKey Tab = "TAB" showKey BackTab = "SHIFT-TAB"-showKey PgUp = "PGUP"-showKey PgDn = "PGDOWN"-showKey Left = "LEFT"-showKey Right = "RIGHT"+showKey BackSpace = "BACKSPACE" showKey Up = "UP" showKey Down = "DOWN"+showKey Left = "LEFT"+showKey Right = "RIGHT"+showKey Home = "HOME" showKey End = "END"+showKey PgUp = "PGUP"+showKey PgDn = "PGDOWN" showKey Begin = "BEGIN"-showKey Home = "HOME" showKey (KP c) = "KEYPAD(" <> T.singleton c <> ")" showKey (Unknown s) = s @@ -167,29 +116,38 @@ -- | Configurable event handler for the direction keys. -- Used for directed commands such as close door.-handleDir :: X -> KM -> (Vector -> a) -> a -> a-handleDir lxsize KM{modifier=NoModifier, key} h k =- let mvs = moves lxsize- assocs = zip dirAllMoveKey $ mvs ++ mvs- in maybe k h (L.lookup key assocs)-handleDir _lxsize _ _h k = k+handleDir :: KM -> (Vector -> a) -> a -> a+handleDir KM{modifier=NoModifier, key} h k =+ let assocs = zip dirAllMoveKey $ moves ++ moves+ in maybe k h (lookup key assocs)+handleDir _ _ k = k -- TODO: deduplicate -- | Binding of both sets of movement keys.-moveBinding :: (VectorXY -> a) -> (VectorXY -> a)+moveBinding :: (Vector -> a) -> (Vector -> a) -> [(KM, a)] moveBinding move run = let assign f (km, dir) = (km, f dir) rNoModifier = repeat NoModifier rControl = repeat Control- in map (assign move) (zip (zipWith KM rNoModifier dirViMoveKey) movesXY) ++- map (assign move) (zip (zipWith KM rNoModifier dirMoveKey) movesXY) ++- map (assign run) (zip (zipWith KM rNoModifier dirViRunKey) movesXY) ++- map (assign run) (zip (zipWith KM rNoModifier dirRunKey) movesXY) ++- map (assign run) (zip (zipWith KM rControl dirMoveKey) movesXY) ++- map (assign run) (zip (zipWith KM rControl dirRunKey) movesXY) ++- map (assign run) (zip (zipWith KM rControl dirHeroKey ) movesXY)+ in map (assign move) (zip (zipWith KM rNoModifier dirViMoveKey) moves) +++ map (assign move) (zip (zipWith KM rNoModifier dirMoveKey) moves) +++ map (assign run) (zip (zipWith KM rNoModifier dirViRunKey) moves) +++ map (assign run) (zip (zipWith KM rNoModifier dirRunKey) moves) +++ map (assign run) (zip (zipWith KM rControl dirMoveKey) moves) +++ map (assign run) (zip (zipWith KM rControl dirRunKey) moves) +++ map (assign run) (zip (zipWith KM rControl dirHeroKey ) moves) +mkKM :: String -> KM+mkKM s = let mkKey sk =+ case keyTranslate sk of+ Unknown _ ->+ assert `failure` "unknown key" `twith` s+ key -> key+ in case s of+ ('C':'T':'R':'L':'-':rest) -> KM {key=mkKey rest, modifier=Control}+ _ -> KM {key=mkKey s, modifier=NoModifier}+ -- | Translate key from a GTK string description to our internal key type. -- To be used, in particular, for the command bindings and macros -- in the config file.@@ -198,6 +156,7 @@ keyTranslate "greater" = Char '>' keyTranslate "period" = Char '.' keyTranslate "colon" = Char ':'+keyTranslate "semicolon" = Char ';' keyTranslate "comma" = Char ',' keyTranslate "question" = Char '?' keyTranslate "dollar" = Char '$'@@ -205,20 +164,24 @@ keyTranslate "KP_Multiply" = Char '*' keyTranslate "slash" = Char '/' keyTranslate "KP_Divide" = Char '/'+keyTranslate "backslash" = Char '\\' keyTranslate "underscore" = Char '_' keyTranslate "minus" = Char '-' keyTranslate "KP_Subtract" = Char '-' keyTranslate "plus" = Char '+' keyTranslate "KP_Add" = Char '+'+keyTranslate "equal" = Char '=' keyTranslate "bracketleft" = Char '[' keyTranslate "bracketright" = Char ']' keyTranslate "braceleft" = Char '{' keyTranslate "braceright" = Char '}'+keyTranslate "apostrophe" = Char '\'' keyTranslate "Escape" = Esc keyTranslate "Return" = Return keyTranslate "space" = Space keyTranslate "Tab" = Tab keyTranslate "ISO_Left_Tab" = BackTab+keyTranslate "BackSpace" = BackSpace keyTranslate "KP_Up" = Up keyTranslate "KP_Down" = Down keyTranslate "KP_Left" = Left
Game/LambdaHack/Common/Kind.hs view
@@ -1,17 +1,16 @@ {-# LANGUAGE GeneralizedNewtypeDeriving, RankNTypes, TypeFamilies #-} -- | General content types and operations. module Game.LambdaHack.Common.Kind- ( -- * General content types- Id, sentinelId, Speedup(..), Ops(..), COps(..), createOps, stdRuleset- -- * Arrays of content identifiers- , Array, (!), (//), listArray, array, bounds, foldlArray+ ( Id, Speedup(..), Ops(..), COps(..), createOps, stdRuleset+ , Tab, createTab, accessTab ) where +import Control.Exception.Assert.Sugar import qualified Data.Array.Unboxed as A import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.Ix as Ix-import qualified Data.List as L+import Data.List import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) import Data.Text (Text)@@ -29,29 +28,37 @@ import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind-import Control.Exception.Assert.Sugar import Game.LambdaHack.Utils.Frequency -- | Content identifiers for the content type @c@. newtype Id c = Id Word8- deriving (Show, Eq, Ord, Ix.Ix, Enum)--instance Binary (Id c) where- put (Id i) = put i- get = fmap Id get--sentinelId :: Id c-sentinelId = Id 255+ deriving (Show, Eq, Ord, Ix.Ix, Enum, Bounded, Binary) -- | Type family for auxiliary data structures for speeding up -- content operations. data family Speedup a data instance Speedup TileKind = TileSpeedup- { isClearTab :: Id TileKind -> Bool- , isLitTab :: Id TileKind -> Bool+ { isClearTab :: !Tab+ , isLitTab :: !Tab+ , isWalkableTab :: !Tab+ , isPassableTab :: !Tab+ , isDoorTab :: !Tab+ , isSuspectTab :: !Tab } +newtype Tab = Tab (A.UArray (Id TileKind) Bool)++createTab :: Ops TileKind -> (TileKind -> Bool) -> Tab+createTab Ops{ofoldrWithKey, obounds} p =+ let f _ k acc = p k : acc+ clearAssocs = ofoldrWithKey f []+ in Tab $ A.listArray obounds clearAssocs++accessTab :: Tab -> Id TileKind -> Bool+{-# INLINE accessTab #-}+accessTab (Tab tab) ki = tab A.! ki+ -- | Content operations for the content of type @a@. data Ops a = Ops { okind :: Id a -> a -- ^ the content element at given id@@ -70,44 +77,44 @@ -- of type @a@. createOps :: forall a. Show a => ContentDef a -> Ops a createOps ContentDef{getName, getFreq, content, validate} =- assert (Id (fromIntegral $ length content) < sentinelId) $+ assert (length content <= fromEnum (maxBound :: Id a)) $ let kindMap :: EM.EnumMap (Id a) a- !kindMap = EM.fromDistinctAscList $ L.zip [Id 0..] content+ !kindMap = EM.fromDistinctAscList $ zip [Id 0..] content kindFreq :: M.Map Text (Frequency (Id a, a)) kindFreq =- let tuples = [ (group, (n, (i, k)))+ let tuples = [ (cgroup, (n, (i, k))) | (i, k) <- EM.assocs kindMap- , (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 <> "')"+ , (cgroup, n) <- getFreq k, n > 0 ]+ f m (cgroup, nik) = M.insertWith (++) cgroup [nik] m+ lists = foldl' f M.empty tuples+ nameFreq cgroup = toFreq $ "opick ('" <> cgroup <> "')" in M.mapWithKey nameFreq lists okind i = fromMaybe (assert `failure` "no kind" `twith` (i, kindMap)) $ EM.lookup i kindMap- correct a = not (T.null (getName a)) && L.all ((> 0) . snd) (getFreq a)+ correct a = not (T.null (getName a)) && all ((> 0) . snd) (getFreq a) offenders = validate content in assert (allB correct content) $- assert (L.null offenders `blame` "content not valid" `twith` offenders)+ assert (null offenders `blame` "content not valid" `twith` offenders) -- By this point 'content' can be GCd. Ops { okind- , ouniqGroup = \group ->+ , ouniqGroup = \cgroup -> let freq = fromMaybe (assert `failure` "no unique group"- `twith` (group, kindFreq))- $ M.lookup group kindFreq+ `twith` (cgroup, kindFreq))+ $ M.lookup cgroup kindFreq in case runFrequency freq of [(n, (i, _))] | n > 0 -> i- l -> assert `failure` "not unique" `twith` (l, group, kindFreq)- , opick = \group p ->- case M.lookup group kindFreq of+ l -> assert `failure` "not unique" `twith` (l, cgroup, kindFreq)+ , opick = \cgroup p ->+ case M.lookup cgroup kindFreq of Just freq | not $ nullFreq freq -> fmap Just $ frequency $ do (i, k) <- freq breturn (p k) i {- with MonadComprehensions:- frequency [ i | (i, k) <- kindFreq M.! group, p k ]+ frequency [ i | (i, k) <- kindFreq M.! cgroup, p k ] -} _ -> return Nothing- , ofoldrWithKey = \f z -> L.foldr (\(i, a) -> f i a) z+ , ofoldrWithKey = \f z -> foldr (\(i, a) -> f i a) z $ EM.assocs kindMap , obounds = ( fst $ EM.findMin kindMap , fst $ EM.findMax kindMap )@@ -135,46 +142,3 @@ instance Eq COps where (==) _ _ = True---- | Arrays of content identifiers pointing to the content type @c@,--- where the identifiers are represented as @Word8@--- (and so content of type @c@ can have at most 256 elements).--- The arrays are indexed by type @i@, e.g., a dungeon tile position.-newtype Array i c = Array (A.UArray i Word8)- deriving Eq---- TODO: save/restore is still too slow, but we are already past--- the point of diminishing returns. A dramatic change would be--- low-level conversion to ByteString and serializing that.-instance (Ix.Ix i, Binary i) => Binary (Array i c) where- put (Array a) = put a- get = fmap Array get--instance (Ix.Ix i, Show i) => Show (Array i c) where- show a = "Kind.Array with bounds " ++ show (bounds a)---- | Content identifiers array lookup.-(!) :: Ix.Ix i => Array i c -> i -> Id c-(!) (Array a) i = Id $ a A.! i---- | Construct a content identifiers array updated with the association list.-(//) :: Ix.Ix i => Array i c -> [(i, Id c)] -> Array i c-(//) (Array a) l = Array $ a A.// [(i, e) | (i, Id e) <- l]---- | Create a content identifiers array from a list of elements.-listArray :: Ix.Ix i => (i, i) -> [Id c] -> Array i c-listArray bds l = Array $ A.listArray bds [e | Id e <- l]---- | Create a content identifiers array from an association list.-array :: Ix.Ix i => (i, i) -> [(i, Id c)] -> Array i c-array bds l = Array $ A.array bds [(i, e) | (i, Id e) <- l]---- | Content identifiers array bounds.-bounds :: Ix.Ix i => Array i c -> (i, i)-bounds (Array a) = A.bounds a---- | Fold left strictly over an array.-foldlArray :: Ix.Ix i => (a -> Id c -> a) -> a -> Array i c -> a-foldlArray f z0 (Array a) = lgo z0 $ A.elems a- where lgo z [] = z- lgo z (x : xs) = let fzx = f z (Id x) in fzx `seq` lgo fzx xs
Game/LambdaHack/Common/Level.hs view
@@ -12,14 +12,16 @@ -- * Level update , updatePrio, updateSmell, updateFloor, updateTile -- * Level query- , at, atI, accessible, accessibleDir, isSecretPos, hideTile+ , at, atI, checkAccess, checkDoorAccess, accessible, accessibleDir+ , isSecretPos, hideTile , findPos, findPosTry, mapLevelActors_, mapDungeonActors_ ) where +import Control.Exception.Assert.Sugar import Data.Binary import qualified Data.Bits as Bits import qualified Data.EnumMap.Strict as EM-import qualified Data.List as L+import Data.Maybe import Data.Text (Text) import GHC.Generics (Generic) @@ -27,7 +29,7 @@ import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY+import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.Tile import qualified Game.LambdaHack.Common.Tile as Tile@@ -35,15 +37,14 @@ import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind-import Control.Exception.Assert.Sugar import Game.LambdaHack.Utils.Frequency -- | The complete dungeon is a map from level names to levels. type Dungeon = EM.EnumMap LevelId Level -- | Levels in the current branch, @k@ levels shallower than the current.-ascendInBranch :: Dungeon -> LevelId -> Int -> [LevelId]-ascendInBranch dungeon lid k =+ascendInBranch :: Dungeon -> Int -> LevelId -> [LevelId]+ascendInBranch dungeon k lid = -- Currently there is just one branch, so the computation is simple. let (minD, maxD) = case (EM.minViewWithKey dungeon, EM.maxViewWithKey dungeon) of@@ -69,7 +70,7 @@ type ItemFloor = EM.EnumMap Point ItemBag -- | Tile kinds on the map.-type TileMap = Kind.Array Point TileKind+type TileMap = PointArray.Array (Kind.Id TileKind) -- | Current smell on map tiles. type SmellMap = EM.EnumMap Point SmellTime@@ -86,15 +87,17 @@ , lsmell :: !SmellMap -- ^ remembered smells on the level , ldesc :: !Text -- ^ level description , lstair :: !([Point], [Point])- -- ^ destinations of (up, down) stairs+ -- ^ destinations of (up, down) stairs , lseen :: !Int -- ^ currently remembered clear tiles- , lclear :: !Int -- ^ total number of initially clear tiles+ , lclear :: !Int -- ^ total number of initially clear tiles;+ -- set to 1 on clients when fully explored , ltime :: !Time -- ^ date of the last activity on the level , litemNum :: !Int -- ^ number of initial items, 0 for clients , litemFreq :: !(Frequency Text) -- ^ frequency of initial items, -- [] for clients , lsecret :: !Int -- ^ secret tile seed , lhidden :: !Int -- ^ secret tile density+ , lescape :: !Bool -- ^ has an Effect.Escape tile } deriving (Show, Eq) @@ -121,23 +124,40 @@ -- | Query for tile kinds on the map. at :: Level -> Point -> Kind.Id TileKind-at Level{ltile} p = ltile Kind.! p+at Level{ltile} p = ltile PointArray.! p -- | Query for items on the ground. atI :: Level -> Point -> ItemBag atI Level{lfloor} p = EM.findWithDefault EM.empty p lfloor +checkAccess :: Kind.COps -> Level -> Maybe (Point -> Point -> Bool)+checkAccess Kind.COps{corule} _ =+ case raccessible $ Kind.stdRuleset corule of+ Nothing -> Nothing+ Just ch -> Just $ \spos tpos -> ch spos tpos++checkDoorAccess :: Kind.COps -> Level -> Maybe (Point -> Point -> Bool)+checkDoorAccess Kind.COps{corule, cotile} lvl =+ case raccessibleDoor $ Kind.stdRuleset corule of+ Nothing -> Nothing+ Just chDoor ->+ Just $ \spos tpos ->+ let st = lvl `at` spos+ tt = lvl `at` tpos+ in not (Tile.isDoor cotile st || Tile.isDoor cotile tt)+ || chDoor spos tpos+ -- | Check whether one position is accessible from another, -- using the formula from the standard ruleset. -- Precondition: the two positions are next to each other. accessible :: Kind.COps -> Level -> Point -> Point -> Bool-accessible Kind.COps{cotile=Kind.Ops{okind=okind}, corule}- lvl@Level{lxsize} spos tpos =- assert (chessDist lxsize spos tpos == 1) $- let check = raccessible $ Kind.stdRuleset corule- src = okind $ lvl `at` spos- tgt = okind $ lvl `at` tpos- in check lxsize spos src tpos tgt+accessible cops@Kind.COps{cotile} lvl =+ let checkWalkability =+ Just $ \_ tpos -> Tile.isWalkable cotile $ lvl `at` tpos+ conditions = catMaybes [ checkWalkability+ , checkAccess cops lvl+ , checkDoorAccess cops lvl ]+ in \spos tpos -> all (\f -> f spos tpos) conditions -- | Check whether actors can move from a position along a unit vector, -- using the formula from the standard ruleset.@@ -158,11 +178,14 @@ -- | Find a random position on the map satisfying a predicate. findPos :: TileMap -> (Point -> Kind.Id TileKind -> Bool) -> Rnd Point findPos ltile p =- let search = do- pos <- randomR $ Kind.bounds ltile- let tile = ltile Kind.! pos+ let (x, y) = PointArray.sizeA ltile+ search = do+ px <- randomR (0, x - 1)+ py <- randomR (0, y - 1)+ let pos = Point{..}+ tile = ltile PointArray.! pos if p pos tile- then return pos+ then return $! pos else search in search @@ -179,12 +202,15 @@ -> Rnd Point findPosTry _ ltile m [] = findPos ltile m findPosTry numTries ltile m l@(_ : tl) = assert (numTries > 0) $- let search 0 = findPosTry numTries ltile m tl+ let (x, y) = PointArray.sizeA ltile+ search 0 = findPosTry numTries ltile m tl search k = do- pos <- randomR $ Kind.bounds ltile- let tile = ltile Kind.! pos- if m pos tile && L.all (\p -> p pos tile) l- then return pos+ px <- randomR (0, x - 1)+ py <- randomR (0, y - 1)+ let pos = Point{..}+ tile = ltile PointArray.! pos+ if m pos tile && all (\p -> p pos tile) l+ then return $! pos else search (k - 1) in search numTries @@ -216,6 +242,7 @@ put litemFreq put lsecret put lhidden+ put lescape get = do ldepth <- get lprio <- get@@ -233,4 +260,5 @@ litemFreq <- get lsecret <- get lhidden <- get- return Level{..}+ lescape <- get+ return $! Level{..}
Game/LambdaHack/Common/Misc.hs view
@@ -2,7 +2,7 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -- | Hacks that haven't found their home yet. module Game.LambdaHack.Common.Misc- ( normalLevelBound, maxLevelDim, divUp, Freqs, breturn+ ( normalLevelBound, divUp, Freqs, breturn , FactionId, LevelId ) where @@ -19,15 +19,11 @@ -- | Level bounds. TODO: query terminal size instead and scroll view. normalLevelBound :: (Int, Int)-normalLevelBound = (79, 21)---- | Maximal supported level X and Y dimension (32768). Not checked anywhere.--- The value is chosen to support architectures with 32-bit ints.-maxLevelDim :: Int-maxLevelDim = 2 ^ (15 :: Int)+normalLevelBound = (79, 20) +infixl 7 `divUp` -- | Integer division, rounding up.-divUp :: Int -> Int -> Int+divUp :: Integral a => a -> a -> a divUp n k = (n + k - 1) `div` k -- | For each group that the kind belongs to, denoted by a @Text@ name
Game/LambdaHack/Common/Msg.hs view
@@ -2,35 +2,39 @@ -- | Game messages displayed on top of the screen for the player to read. module Game.LambdaHack.Common.Msg ( makePhrase, makeSentence- , Msg, (<>), (<+>), showT, moreMsg, yesnoMsg, truncateMsg+ , Msg, (<>), (<+>), tshow, toWidth, moreMsg, yesnoMsg, truncateMsg , Report, emptyReport, nullReport, singletonReport, addMsg , splitReport, renderReport, findInReport , History, emptyHistory, singletonHistory, mergeHistory , addReport, renderHistory, takeHistory- , Overlay, stringByLocation- , Slideshow(runSlideshow), splitOverlay, toSlideshow)+ , Overlay(overlay), emptyOverlay, truncateToOverlay, toOverlay+ , Slideshow(slideshow), splitOverlay, toSlideshow) where import Data.Binary import qualified Data.ByteString.Char8 as BS-import Data.Char-import qualified Data.EnumMap.Strict as EM import Data.List-import Data.Monoid hiding ((<>))+import Data.Monoid import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8)-import NLP.Miniutter.English ((<+>), (<>)) import qualified NLP.Miniutter.English as MU import qualified Text.Show.Pretty as Show.Pretty import Game.LambdaHack.Common.Misc-import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Point +infixr 6 <+> -- TODO: not needed when we require a very new minimorph+(<+>) :: Text -> Text -> Text+(<+>) = (MU.<+>)+ -- Pretty print and pack the result of @show@.-showT :: Show a => a -> Text-showT x = T.pack $ Show.Pretty.ppShow x+tshow :: Show a => a -> Text+tshow x = T.pack $ Show.Pretty.ppShow x +toWidth :: Int -> Text -> Text+toWidth n x = T.take n (T.justifyLeft n ' ' x)+ -- | Re-exported English phrase creation functions, applied to default -- irregular word sets. makePhrase, makeSentence :: [MU.Part] -> Text@@ -56,7 +60,7 @@ let xs = case T.lines xsRaw of [] -> xsRaw [line] -> line- line : _ -> line <> T.replicate (w + 1) " "+ line : _ -> T.justifyLeft (w + 1) ' ' line len = T.length xs in case compare w len of LT -> T.snoc (T.take (w - 1) xs) '$'@@ -67,11 +71,7 @@ -- | The type of a set of messages to show at the screen at once. newtype Report = Report [(BS.ByteString, Int)]- deriving (Show)--instance Binary Report where- put (Report x) = put x- get = fmap Report get+ deriving (Show, Binary) -- | Empty set of messages. emptyReport :: Report@@ -96,11 +96,12 @@ -- | 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 -> [Text]-splitReport r =- let w = fst normalLevelBound + 1- in splitText w $ renderReport r+splitReport :: X -> Report -> Overlay+splitReport w r = Overlay $ splitReportList w r +splitReportList :: X -> Report -> [Text]+splitReportList w r = splitText w $ renderReport r+ -- | Render a report as a (possibly very long) string. renderReport :: Report -> Text renderReport (Report []) = T.empty@@ -109,7 +110,7 @@ renderRepetition :: (BS.ByteString, Int) -> Text renderRepetition (s, 1) = decodeUtf8 s-renderRepetition (s, n) = decodeUtf8 s <> "<x" <> showT n <> ">"+renderRepetition (s, n) = decodeUtf8 s <> "<x" <> tshow n <> ">" findInReport :: (BS.ByteString -> Bool) -> Report -> Maybe BS.ByteString findInReport f (Report xns) = find f $ map fst xns@@ -118,27 +119,22 @@ -- other than whitespace or punctuation. Space characters are removed -- from the start, but never from the end of lines. Newlines are respected. splitText :: X -> Text -> [Text]-splitText w xs = concatMap (splitText' w . T.dropWhile isSpace) $ T.lines xs+splitText w xs = concatMap (splitText' w . T.stripStart) $ T.lines xs splitText' :: X -> Text -> [Text] splitText' w xs- | w <= 0 = [xs] -- border case, we cannot make progress | w >= T.length xs = [xs] -- no problem, everything fits | otherwise = let (pre, post) = T.splitAt w xs (ppre, ppost) = T.break (== ' ') $ T.reverse pre- testPost = T.dropWhile isSpace ppost+ testPost = T.stripEnd 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]- deriving Show--instance Binary History where- put (History x) = put x- get = fmap History get+ deriving (Show, Binary) -- | Empty history of reports. emptyHistory :: History@@ -156,7 +152,9 @@ -- | Render history as many lines of text, wrapping if necessary. renderHistory :: History -> Overlay-renderHistory (History h) = concatMap splitReport h+renderHistory (History h) =+ let w = fst normalLevelBound + 1+ in Overlay $ concatMap (splitReportList w) h -- | Add a report to history, handling repetitions. addReport :: Report -> History -> History@@ -176,48 +174,47 @@ -- | A series of screen lines that may or may not fit the width nor height -- of the screen. An overlay may be transformed by adding the first line -- and/or by splitting into a slideshow of smaller overlays.-type Overlay = [Text]+newtype Overlay = Overlay {overlay :: [Text]}+ deriving (Show, Eq, Binary) --- | Returns a function that looks up the characters in the--- string by position. 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- -> (Text, PointXY -> Maybe Char, Maybe Text)-stringByLocation _ _ [] = (T.empty, const Nothing, Nothing)-stringByLocation lxsize lysize (msgTop : ls) =- let (over, bottom) = splitAt lysize $ map (truncateMsg lxsize) ls- m = EM.fromDistinctAscList- $ zip [0..]- (map (EM.fromDistinctAscList . zip [0..] . T.unpack) over)- msgBottom = case bottom of- [] -> Nothing- [s] -> Just s- _ -> Just "--a portion of the text trimmed--"- in (truncateMsg lxsize msgTop,- \ (PointXY (x, y)) -> EM.lookup y m >>= \ n -> EM.lookup x n,- msgBottom)+emptyOverlay :: Overlay+emptyOverlay = Overlay [] +truncateToOverlay :: X -> Text -> Overlay+truncateToOverlay lxsize msg = Overlay [truncateMsg lxsize msg]++toOverlay :: [Text] -> Overlay+toOverlay = Overlay+ -- | Split an overlay into a slideshow in which each overlay, -- prefixed by @msg@ and postfixed by @moreMsg@ except for the last one,--- fits on the screen wrt height (but lines may still be too wide).-splitOverlay :: Y -> Overlay -> Overlay -> Slideshow-splitOverlay lysize msg ls =+-- fits on the screen wrt height (but lines may be too wide).+splitOverlay :: Bool -> Y -> Overlay -> Overlay -> Slideshow+splitOverlay onBlank yspace (Overlay msg) (Overlay ls) = let over = msg ++ ls- in if length over <= lysize + 2- then Slideshow [over] -- all fits on one screen- else let (pre, post) = splitAt (lysize + 1) over- Slideshow slides = splitOverlay lysize msg post- in Slideshow $ (pre ++ [moreMsg]) : slides+ in if length over <= yspace+ then Slideshow (onBlank, [Overlay over]) -- all fits on one screen+ else let (pre, post) = splitAt (yspace - 1) over+ Slideshow (_, slides) =+ splitOverlay onBlank yspace (Overlay msg) (Overlay post)+ in Slideshow $ (onBlank, (Overlay $ pre ++ [moreMsg]) : slides) -- | A few overlays, displayed one by one upon keypress. -- When displayed, they are trimmed, not wrapped -- and any lines below the lower screen edge are not visible.-newtype Slideshow = Slideshow {runSlideshow :: [Overlay]}- deriving (Monoid, Show)+-- If the boolean flag is set, the overlay is displayed over a blank screen,+-- including the bottom lines.+newtype Slideshow = Slideshow {slideshow :: (Bool, [Overlay])}+ deriving Show --- | Declare the list of overlays to be fit for display on the screen.+instance Monoid Slideshow where+ mempty = Slideshow (False, [])+ mappend (Slideshow (b1, l1)) (Slideshow (b2, l2)) =+ Slideshow (b1 || b2, l1 ++ l2)++-- | Declare the list of raw overlays to be fit for display on the screen. -- In particular, current @Report@ is eiter empty or unimportant -- or contained in the overlays and if any vertical or horizontal -- trimming of the overlays happens, this is intended.-toSlideshow :: [Overlay] -> Slideshow-toSlideshow = Slideshow+toSlideshow :: Bool -> [[Text]] -> Slideshow+toSlideshow onBlank l = Slideshow (onBlank, map Overlay l)
Game/LambdaHack/Common/Perception.hs view
@@ -17,9 +17,9 @@ -- the tile, so the player can flee or block. Invisible actors in open -- space can be hit. module Game.LambdaHack.Common.Perception- ( Perception(..), PerceptionVisible(..), PerActor+ ( Perception(Perception), PerceptionVisible(PerceptionVisible) , totalVisible, smellVisible- , actorSeesPos, nullPer, addPer, diffPer, smellFromActors+ , nullPer, addPer, diffPer , FactionPers, Pers ) where @@ -28,30 +28,20 @@ import qualified Data.EnumSet as ES import GHC.Generics (Generic) -import Game.LambdaHack.Common.Actor-import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction-import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.State-import Game.LambdaHack.Content.ActorKind --- TOOD: if really needed, optimize by representing as a set of intervals. newtype PerceptionVisible = PerceptionVisible- { pvisible :: ES.EnumSet Point}+ {pvisible :: ES.EnumSet Point} deriving (Show, Eq, Binary) -type PerActor = EM.EnumMap ActorId PerceptionVisible-+-- TOOD: if really needed, optimize by representing as a set of intervals+-- or a set of bitmaps, like the internal representation of IntSet. -- | The type representing the perception of a faction on a level.--- The total visibility holds the sum of FOVs of all actors--- of a given faction on the level and serves only as a speedup.--- The fields are not strict because often not all are used. data Perception = Perception- { perActor :: !PerActor -- ^ visible points for each actor- , ptotal :: !PerceptionVisible -- ^ sum over all actors- , psmell :: !PerceptionVisible -- ^ sum over actors that can smell+ { ptotal :: !PerceptionVisible -- ^ sum over all actors+ , psmell :: !PerceptionVisible -- ^ sum over actors that can smell } deriving (Show, Eq, Generic) @@ -72,52 +62,23 @@ smellVisible :: Perception -> ES.EnumSet Point smellVisible = pvisible . psmell --- | Whether an actor can see a position.-actorSeesPos :: Perception -> ActorId -> Point -> Bool-actorSeesPos per aid pos =- let isIn = (pos `ES.member`) . pvisible- -- Blind and non-smelling actors don't see their own pos, hence False.- in maybe False isIn $ EM.lookup aid $ perActor per- nullPer :: Perception -> Bool-nullPer per = ES.null (totalVisible per)+nullPer per = ES.null (totalVisible per) && ES.null (smellVisible per) addPer :: Perception -> Perception -> Perception addPer per1 per2 =- let f :: (PerceptionVisible -> PerceptionVisible -> PerceptionVisible)- f pv1 pv2 = PerceptionVisible $ pvisible pv1 `ES.union` pvisible pv2- in Perception- { perActor = EM.unionWith f (perActor per1) (perActor per2)- , ptotal = PerceptionVisible- $ totalVisible per1 `ES.union` totalVisible per2- , psmell = PerceptionVisible- $ smellVisible per1 `ES.union` smellVisible per2- }+ Perception+ { ptotal = PerceptionVisible+ $ totalVisible per1 `ES.union` totalVisible per2+ , psmell = PerceptionVisible+ $ smellVisible per1 `ES.union` smellVisible per2+ } diffPer :: Perception -> Perception -> Perception diffPer per1 per2 =- let f :: (PerceptionVisible -> PerceptionVisible -> Maybe PerceptionVisible)- f pv1 pv2 =- let diff = pvisible pv1 ES.\\ pvisible pv2- in if ES.null diff then Nothing else Just $ PerceptionVisible diff- in Perception- { perActor = EM.differenceWith f (perActor per1) (perActor per2)- , ptotal = PerceptionVisible- $ totalVisible per1 ES.\\ totalVisible per2- , psmell = PerceptionVisible- $ smellVisible per1 ES.\\ smellVisible per2- }--smellFromActors :: Kind.COps -> State -> PerActor -> PerceptionVisible-smellFromActors Kind.COps{coactor=Kind.Ops{okind}} s perActor =- let actorCanSmell aid =- -- If actor is just created, it can already be in Perception- -- sent to the client, but not in the client's state.- -- We assume the actor's nose doesn't work yet on first turn.- -- TODO: assume so on the server, too, or overhaul smelling again.- not (EM.notMember aid $ sactorD s)- && let b = getActorBody aid s- in asmell $ okind $ bkind b- visSmell = filter (actorCanSmell . fst) $ EM.assocs perActor- setSmell = map (pvisible . snd) visSmell- in PerceptionVisible $ ES.unions setSmell+ Perception+ { ptotal = PerceptionVisible+ $ totalVisible per1 ES.\\ totalVisible per2+ , psmell = PerceptionVisible+ $ smellVisible per1 ES.\\ smellVisible per2+ }
Game/LambdaHack/Common/Point.hs view
@@ -1,113 +1,136 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Basic operations on 2D points represented as linear offsets. module Game.LambdaHack.Common.Point- ( Point, toPoint, showPoint- , origin, chessDist, adjacent, vicinity, vicinityCardinal- , inside, displacementXYZ, bla+ ( X, Y, Point(..), maxLevelDimExponent+ , chessDist, euclidDistSq, adjacent, inside, bla, fromTo ) where +import Control.Exception.Assert.Sugar import Data.Binary-import qualified Data.Ix as Ix-import qualified Data.List as L-import Data.Text (Text)-import qualified System.Random as R+import Data.Bits (unsafeShiftL, unsafeShiftR, (.&.))+import Data.Int (Int32) -import Game.LambdaHack.Common.Msg-import Game.LambdaHack.Common.PointXY-import Game.LambdaHack.Common.VectorXY-import Control.Exception.Assert.Sugar+-- | Spacial dimension for points and vectors.+type X = Int --- | The type of positions on the 2D level map, heavily optimized.------ We represent the (level map on the) screen as a linear framebuffer,--- where @Point@ is an @Int@ offset counted from the first cell.--- We do bounds check for the X size whenever we convert--- between representations and each subsequent array access--- performs another check, effectively for Y size.--- After dungeon is generated (using @PointXY@, not @Point@),--- and converted to the @Point@ representation, points are used--- mainly as keys and not constructed often, so the performance will improve--- due to smaller save files, the use of @EnumMap@ and cheaper array indexing,--- including cheaper bounds checks.-newtype Point = Point Int- deriving (Eq, Ord, Ix.Ix, Enum, R.Random)+-- | Spacial dimension for points and vectors.+type Y = Int -instance Binary Point where- put (Point n) = put n- get = fmap Point get+-- | 2D points in cartesian representation. Coordinates grow to the right+-- and down, so that the (0, 0) point is in the top-left corner of the screen.+-- Coordinates are never negative.+data Point = Point+ { px :: !X+ , py :: !Y+ }+ deriving (Eq, Ord) --- For debugging. instance Show Point where- show (Point n) = show n+ show (Point x y) = show (x, y) --- | Print a point as a tuple of cartesian coordinates.-showPoint :: X -> Point -> Text-showPoint lxsize = showT . fromPoint lxsize+instance Binary Point where+ put = put . (fromIntegral :: Int -> Int32) . fromEnum+ get = fmap (toEnum . (fromIntegral :: Int32 -> Int)) get --- | Conversion from cartesian coordinates to @Point@.-toPoint :: X -> PointXY -> Point-toPoint lxsize (PointXY (x, y)) =- assert (lxsize > x && x >= 0 && y >= 0 `blame` "invalid point coordinates"- `twith` (lxsize, x, y))- $ Point $ x + y * lxsize+-- This conversion cannot be used for PointArray indexing,+-- because it is not contiguous --- we don't know the horizontal+-- width of the levels nor of the screen.+-- The conversion is implemented mainly for @EnumMap@ and @EnumSet@.+instance Enum Point where+ fromEnum = fromEnumPoint+ toEnum = toEnumPoint --- | Conversion from @Point@ to cartesian coordinates.-fromPoint :: X -> Point -> PointXY-fromPoint lxsize (Point p) =- assert (p >= 0 `blame` "negative point value" `twith` (lxsize, p))- $ PointXY (p `rem` lxsize, p `quot` lxsize)+-- | The maximum number of bits for level X and Y dimension (16).+-- The value is chosen to support architectures with 32-bit Ints.+maxLevelDimExponent :: Int+{-# INLINE maxLevelDimExponent #-}+maxLevelDimExponent = 16 --- | The top-left corner position of the level.-origin :: Point-origin = Point 0+-- | Maximal supported level X and Y dimension. Not checked anywhere.+-- The value is chosen to support architectures with 32-bit Ints.+maxLevelDim :: Int+{-# INLINE maxLevelDim #-}+maxLevelDim = 2 ^ maxLevelDimExponent - 1 +fromEnumPoint :: Point -> Int+{-# INLINE fromEnumPoint #-}+fromEnumPoint (Point x y) =+ assert (x >= 0 && y >= 0 `blame` "invalid point coordinates"+ `twith` (x, y))+ $ x + unsafeShiftL y maxLevelDimExponent++toEnumPoint :: Int -> Point+{-# INLINE toEnumPoint #-}+toEnumPoint n =+ Point (n .&. maxLevelDim) (unsafeShiftR n maxLevelDimExponent)+ -- | The distance between two points in the chessboard metric.-chessDist :: X -> Point -> Point -> Int-chessDist lxsize pos0 pos1- | PointXY (x0, y0) <- fromPoint lxsize pos0- , PointXY (x1, y1) <- fromPoint lxsize pos1 =- chessDistXY $ VectorXY (x1 - x0, y1 - y0)+chessDist :: Point -> Point -> Int+{-# INLINE chessDist #-}+chessDist (Point x0 y0) (Point x1 y1) = max (abs (x1 - x0)) (abs (y1 - y0)) +-- | Squared euclidean distance between two points.+euclidDistSq :: Point -> Point -> Int+{-# INLINE euclidDistSq #-}+euclidDistSq (Point x0 y0) (Point x1 y1) =+ let square n = n ^ (2 :: Int)+ in square (x1 - x0) + square (y1 - y0)+ -- | Checks whether two points are adjacent on the map -- (horizontally, vertically or diagonally).-adjacent :: X -> Point -> Point -> Bool-adjacent lxsize s t = chessDist lxsize s t == 1---- | Returns the 8, or less, surrounding positions of a given position.-vicinity :: X -> Y -> Point -> [Point]-vicinity lxsize lysize p =- map (toPoint lxsize) $- vicinityXY (0, 0, lxsize - 1, lysize - 1) $- fromPoint lxsize p---- | Returns the 4, or less, surrounding positions in cardinal directions--- from a given position.-vicinityCardinal :: X -> Y -> Point -> [Point]-vicinityCardinal lxsize lysize p =- map (toPoint lxsize) $- vicinityCardinalXY (0, 0, lxsize - 1, lysize - 1) $- fromPoint lxsize p+adjacent :: Point -> Point -> Bool+{-# INLINE adjacent #-}+adjacent s t = chessDist s t == 1 -- | Checks that a point belongs to an area.-inside :: X -> Point -> (X, Y, X, Y) -> Bool-inside lxsize p = insideXY $ fromPoint lxsize p---- | Calculate the displacement vector from a position to another.-displacementXYZ :: X -> Point -> Point -> VectorXY-displacementXYZ lxsize pos0 pos1- | PointXY (x0, y0) <- fromPoint lxsize pos0- , PointXY (x1, y1) <- fromPoint lxsize pos1 =- VectorXY (x1 - x0, y1 - y0)+inside :: Point -> (X, Y, X, Y) -> Bool+{-# INLINE inside #-}+inside (Point x y) (x0, y0, x1, y1) = x1 >= x && x >= x0 && y1 >= y && y >= y0 -- | Bresenham's line algorithm generalized to arbitrary starting @eps@ -- (@eps@ value of 0 gives the standard BLA). -- Skips the source point and goes through the second point -- to the edge of the level. GIves @Nothing@ if the points are equal.+-- The target is given as @Point@ to permit aiming out of the level,+-- e.g., to get uniform distributions of directions for explosions+-- close to the edge of the level. bla :: X -> Y -> Int -> Point -> Point -> Maybe [Point]-bla _ _ _ source target | source == target = Nothing-bla lxsize lysize eps source target = Just $- let s = fromPoint lxsize source- e = fromPoint lxsize target- inBounds p@(PointXY (x, y)) =- lxsize > x && x >= 0 && lysize > y && y >= 0 && p /= s- in L.map (toPoint lxsize) $ L.takeWhile inBounds $ L.tail $ blaXY eps s e+bla lxsize lysize eps source target =+ if source == target then Nothing+ else Just $+ let inBounds p@(Point x y) =+ lxsize > x && x >= 0 && lysize > y && y >= 0 && p /= source+ in takeWhile inBounds $ tail $ blaXY eps source target++-- | Bresenham's line algorithm generalized to arbitrary starting @eps@+-- (@eps@ value of 0 gives the standard BLA). Includes the source point+-- and goes through the target point to infinity.+blaXY :: Int -> Point -> Point -> [Point]+blaXY eps (Point x0 y0) (Point x1 y1) =+ let (dx, dy) = (x1 - x0, y1 - y0)+ xyStep b (x, y) = (x + signum dx, y + signum dy * b)+ yxStep b (x, y) = (x + signum dx * b, y + signum dy)+ (p, q, step) | abs dx > abs dy = (abs dy, abs dx, xyStep)+ | otherwise = (abs dx, abs dy, yxStep)+ bw = balancedWord p q (eps `mod` max 1 q)+ walk w xy = xy : walk (tail w) (step (head w) xy)+ in map (uncurry Point) $ walk bw (x0, y0)++-- | See <http://roguebasin.roguelikedevelopment.org/index.php/Digital_lines>.+balancedWord :: Int -> Int -> Int -> [Int]+balancedWord p q eps | eps + p < q = 0 : balancedWord p q (eps + p)+balancedWord p q eps = 1 : balancedWord p q (eps + p - q)++-- | A list of all points on a straight vertical or straight horizontal line+-- between two points. Fails if no such line exists.+fromTo :: Point -> Point -> [Point]+fromTo (Point x0 y0) (Point x1 y1) =+ let fromTo1 :: Int -> Int -> [Int]+ fromTo1 z0 z1+ | z0 <= z1 = [z0..z1]+ | otherwise = [z0,z0-1..z1]+ result+ | x0 == x1 = map (\ y -> Point x0 y) (fromTo1 y0 y1)+ | y0 == y1 = map (\ x -> Point x y0) (fromTo1 x0 x1)+ | otherwise = assert `failure` "diagonal fromTo"+ `twith` ((x0, y0), (x1, y1))+ in result
+ Game/LambdaHack/Common/PointArray.hs view
@@ -0,0 +1,133 @@+-- | Arrays, based on Data.Vector.Unboxed, indexed by @Point@.+module Game.LambdaHack.Common.PointArray+ ( Array+ , (!), (//), replicateA, replicateMA, generateMA, sizeA+ , foldlA, ifoldlA, minIndexA, minLastIndexA, maxIndexA, maxLastIndexA+ ) where++import Control.Arrow ((***))+import Control.Monad+import Data.Binary+import Data.Vector.Binary ()+import qualified Data.Vector.Fusion.Stream as Stream+import qualified Data.Vector.Generic as G+import qualified Data.Vector.Unboxed as U++import Game.LambdaHack.Common.Point++-- TODO: for now, until there's support for GeneralizedNewtypeDeriving+-- for Unboxed, there's a lot of @Word8@ in place of @c@ here+-- and a contraint @Enum c@ instead of @Unbox c@.++-- TODO: perhaps make them an instance of Data.Vector.Generic?+-- | Arrays indexed by @Point@.+data Array c = Array+ { axsize :: X+ , aysize :: Y+ , avector :: U.Vector Word8+ }+ deriving Eq++instance Show (Array c) where+ show a = "PointArray.Array with size " ++ show (sizeA a)++cnv :: (Enum a, Enum b) => a -> b+cnv = toEnum . fromEnum++pindex :: X -> Point -> Int+pindex xsize (Point x y) = x + y * xsize++punindex :: X -> Int -> Point+punindex xsize n = let (y, x) = n `quotRem` xsize+ in Point x y++-- Note: there's no point specializing this to @Point@ arguments,+-- since the extra few additions in @fromPoint@ may be less expensive than+-- memory or register allocations needed for the extra @Int@ in @Point@.+-- | Array lookup.+(!) :: Enum c => Array c -> Point -> c+{-# INLINE (!) #-}+(!) Array{..} p = cnv $ avector U.! pindex axsize p++-- | Construct an array updated with the association list.+(//) :: Enum c => Array c -> [(Point, c)] -> Array c+(//) Array{..} l = let v = avector U.// map (pindex axsize *** cnv) l+ in Array{avector = v, ..}++-- | Create an array from a replicated element.+replicateA :: Enum c => X -> Y -> c -> Array c+replicateA axsize aysize c =+ Array{avector = U.replicate (axsize * aysize) $ cnv c, ..}++-- | Create an array from a replicated monadic action.+replicateMA :: Enum c => Monad m => X -> Y -> m c -> m (Array c)+replicateMA axsize aysize m = do+ v <- U.replicateM (axsize * aysize) $ liftM cnv m+ return $! Array{avector = v, ..}++-- | Create an array from a monadic function.+generateMA :: Enum c => Monad m => X -> Y -> (Point -> m c) -> m (Array c)+generateMA axsize aysize fm = do+ let gm n = liftM cnv $ fm $ punindex axsize n+ v <- U.generateM (axsize * aysize) gm+ return $! Array{avector = v, ..}++-- | Content identifiers array size.+sizeA :: Array c -> (X, Y)+sizeA Array{..} = (axsize, aysize)++-- | Fold left strictly over an array.+foldlA :: Enum c => (a -> c -> a) -> a -> Array c -> a+foldlA f z0 Array{..} =+ U.foldl' (\a c -> f a (cnv c)) z0 avector++-- | Fold left strictly over an array+-- (function applied to each element and its index).+ifoldlA :: Enum c => (a -> Point -> c -> a) -> a -> Array c -> a+ifoldlA f z0 Array{..} =+ U.ifoldl' (\a n c -> f a (punindex axsize n) (cnv c)) z0 avector++-- | Yield the point coordinates of a minimum element of the array.+-- The array may not be empty.+minIndexA :: Enum c => Array c -> Point+{-# INLINE minIndexA #-}+minIndexA Array{..} = punindex axsize $ U.minIndex avector++-- | Yield the point coordinates of the last minimum element of the array.+-- The array may not be empty.+minLastIndexA :: Enum c => Array c -> Point+{-# INLINE minLastIndexA #-}+minLastIndexA Array{..} =+ punindex axsize+ $ fst . Stream.foldl1' imin . Stream.indexed . G.stream+ $ avector+ where+ imin (i, x) (j, y) = i `seq` j `seq` if x >= y then (j, y) else (i, x)++-- | Yield the point coordinates of the first maximum element of the array.+-- The array may not be empty.+maxIndexA :: Enum c => Array c -> Point+{-# INLINE maxIndexA #-}+maxIndexA Array{..} = punindex axsize $ U.maxIndex avector++-- | Yield the point coordinates of the last maximum element of the array.+-- The array may not be empty.+maxLastIndexA :: Enum c => Array c -> Point+{-# INLINE maxLastIndexA #-}+maxLastIndexA Array{..} =+ punindex axsize+ $ fst . Stream.foldl1' imax . Stream.indexed . G.stream+ $ avector+ where+ imax (i, x) (j, y) = i `seq` j `seq` if x <= y then (j, y) else (i, x)++instance Binary (Array c) where+ put Array{..} = do+ put axsize+ put aysize+ put avector+ get = do+ axsize <- get+ aysize <- get+ avector <- get+ return $! Array{..}
− Game/LambdaHack/Common/PointXY.hs
@@ -1,79 +0,0 @@--- | Basic cartesian geometry operations on 2D points.-module Game.LambdaHack.Common.PointXY- ( X, Y, PointXY(..), fromTo, sortPointXY, blaXY, insideXY- ) where--import qualified Data.List as L--import Game.LambdaHack.Common.Misc-import Control.Exception.Assert.Sugar---- | Spacial dimension for points and vectors.-type X = Int---- | Spacial dimension for points and vectors.-type Y = Int---- | 2D points in cartesian representation.-newtype PointXY = PointXY (X, Y)- deriving (Eq, Ord)--instance Show PointXY where- show (PointXY (x, y)) = show (x, y)---- TODO: perhaps use this instead of Point, but then @shift@ is not longer--- so cheap, and we need, e.g., an extra addition per FOV point and per--- AI speculative move. Additions are cheap though and code would be--- shorter thanks to removing the lxsize argument in many places.--- More serious is one addition and one multiplication per EnumMap lookup,--- though in the computation-intensive cases of FOV and AI, the extra--- operations were already there, performed before lookup.--- TODO: rem and quot by 2^w can probably be optimised-instance Enum PointXY where- toEnum p = PointXY (p `rem` maxLevelDim, p `quot` maxLevelDim)- fromEnum (PointXY (x, y)) = x + y * maxLevelDim---- | A list of all points on a straight vertical or straight horizontal line--- between two points. Fails if no such line exists.-fromTo :: PointXY -> PointXY -> [PointXY]-fromTo (PointXY (x0, y0)) (PointXY (x1, y1)) =- let result- | x0 == x1 = L.map (\ y -> PointXY (x0, y)) (fromTo1 y0 y1)- | y0 == y1 = L.map (\ x -> PointXY (x, y0)) (fromTo1 x0 x1)- | otherwise = assert `failure` "diagononal fromTo"- `twith` ((x0, y0), (x1, y1))- in result--fromTo1 :: Int -> Int -> [Int]-fromTo1 x0 x1- | x0 <= x1 = [x0..x1]- | otherwise = [x0,x0-1..x1]---- | Sort the sequence of two points, in the derived lexicographic order.-sortPointXY :: (PointXY, PointXY) -> (PointXY, PointXY)-sortPointXY (a, b) | a <= b = (a, b)- | otherwise = (b, a)---- | See <http://roguebasin.roguelikedevelopment.org/index.php/Digital_lines>.-balancedWord :: Int -> Int -> Int -> [Int]-balancedWord p q eps | eps + p < q = 0 : balancedWord p q (eps + p)-balancedWord p q eps = 1 : balancedWord p q (eps + p - q)---- | Bresenham's line algorithm generalized to arbitrary starting @eps@--- (@eps@ value of 0 gives the standard BLA). Includes the source point--- and goes through the target point to infinity.-blaXY :: Int -> PointXY -> PointXY -> [PointXY]-blaXY eps (PointXY (x0, y0)) (PointXY (x1, y1)) =- let (dx, dy) = (x1 - x0, y1 - y0)- xyStep b (x, y) = (x + signum dx, y + signum dy * b)- yxStep b (x, y) = (x + signum dx * b, y + signum dy)- (p, q, step) | abs dx > abs dy = (abs dy, abs dx, xyStep)- | otherwise = (abs dx, abs dy, yxStep)- bw = balancedWord p q (eps `mod` max 1 q)- walk w xy = xy : walk (tail w) (step (head w) xy)- in L.map PointXY $ walk bw (x0, y0)---- | Checks that a point belongs to an area.-insideXY :: PointXY -> (X, Y, X, Y) -> Bool-insideXY (PointXY (x, y)) (x0, y0, x1, y1) =- x1 >= x && x >= x0 && y1 >= y && y >= y0
Game/LambdaHack/Common/Random.hs view
@@ -17,17 +17,15 @@ , rndToIO ) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Control.Monad.State as St import Data.Binary-import qualified Data.Binary as Binary import qualified Data.Hashable as Hashable-import qualified Data.List as L import Data.Ratio import GHC.Generics (Generic) import qualified System.Random as R -import Control.Exception.Assert.Sugar import Game.LambdaHack.Utils.Frequency -- | The monad of computations with random generator state.@@ -59,7 +57,7 @@ -- | Dice: 1d7, 3d3, 1d0, etc. -- @RollDice a b@ represents @a@ rolls of @b@-sided die.-data RollDice = RollDice !Binary.Word8 !Binary.Word8+data RollDice = RollDice !Word8 !Word8 deriving (Eq, Ord, Generic) instance Show RollDice where@@ -67,7 +65,7 @@ instance Read RollDice where readsPrec d s =- let (a, db) = L.break (== 'd') s+ let (a, db) = break (== 'd') s av = read a in case db of 'd' : b -> [ (RollDice av bv, rest) | (bv, rest) <- readsPrec d b ]@@ -84,7 +82,7 @@ -- | Cast dice and sum the results. castDice :: RollDice -> Rnd Int-castDice (RollDice a' 1) = return $ fromEnum a' -- optimization+castDice (RollDice a' 1) = return $! fromEnum a' -- optimization castDice (RollDice a' b') = let (a, b) = (fromEnum a', fromEnum b') in liftM sum (replicateM a (cast b))@@ -139,8 +137,10 @@ -- To the result of rolling the first set of dice we add the second, -- scaled in proportion to current depth divided by maximal dungeon depth. data RollDeep = RollDeep !RollDice !RollDice- deriving Show+ deriving (Show, Eq, Ord, Generic) +instance Binary RollDeep+ rollDeep :: (Int, Int) -> (Int, Int) -> RollDeep rollDeep (a, b) (c, d) = RollDeep (rollDice a b) (rollDice c d) @@ -154,7 +154,7 @@ `twith` (n, depth)) skip r1 <- castDice d1 r2 <- castDice d2- return $ r1 + ((n - 1) * r2) `div` max 1 (depth - 1)+ return $! r1 + ((n - 1) * r2) `div` max 1 (depth - 1) -- | Cast dice scaled with current level depth and return @True@ -- if the results if greater than 50.@@ -163,15 +163,16 @@ let n = abs n' depth = abs depth' c <- castDeep n depth deep- return $ c > 50+ return $! c > 50 -- | Generate a @RollDeep@ that always gives a constant integer. intToDeep :: Int -> RollDeep intToDeep 0 = RollDeep (RollDice 0 0) (RollDice 0 0)-intToDeep n' = let n = toEnum n'- in if n > maxBound || n < minBound- then assert `failure` "Deep out of bound" `twith` n'- else RollDeep (RollDice n 1) (RollDice 0 0)+intToDeep n' = if n' > fromEnum (maxBound :: Word8)+ || n' < fromEnum (minBound :: Word8)+ then assert `failure` "Deep out of bound" `twith` n'+ else let n = toEnum n'+ in RollDeep (RollDice n 1) (RollDice 0 0) -- | Maximal value of scaled dice. maxDeep :: RollDeep -> Int
Game/LambdaHack/Common/Save.hs view
@@ -45,7 +45,9 @@ ms <- takeMVar toSave case ms of Just s -> do- encodeEOF (saveFile s) s+ dataDir <- appDataDir+ tryCreateDir (dataDir </> "saves")+ encodeEOF (dataDir </> "saves" </> saveFile s) s -- Wait until the save finished. During that time, the mvar -- is continually updated to newest state values. loop@@ -77,14 +79,14 @@ -- | Restore a saved game, if it exists. Initialize directory structure -- and cope over data files, if needed. restoreGame :: Binary a- => String -> FilePath- -> [(FilePath, FilePath)] -> (FilePath -> IO FilePath)+ => String -> [(FilePath, FilePath)] -> (FilePath -> IO FilePath) -> IO (Maybe a)-restoreGame name configAppDataDir copies pathsDataFile = do+restoreGame name copies pathsDataFile = do -- Create user data directory and copy files, if not already there.- tryCreateDir configAppDataDir- tryCopyDataFiles configAppDataDir pathsDataFile copies- let saveFile = configAppDataDir </> name+ dataDir <- appDataDir+ tryCreateDir dataDir+ tryCopyDataFiles dataDir pathsDataFile copies+ let saveFile = dataDir </> "saves" </> name saveExists <- doesFileExist saveFile -- If the savefile exists but we get IO or decoding errors, -- we show them and start a new game. If the savefile was randomly@@ -99,7 +101,7 @@ let handler :: Ex.SomeException -> IO (Maybe a) handler e = do let msg = "Restore failed. The error message is:"- <+> (T.unwords . T.lines) (showT e)+ <+> (T.unwords . T.lines) (tshow e) delayPrint $ msg return Nothing either handler return res
Game/LambdaHack/Common/ServerCmd.hs view
@@ -2,7 +2,7 @@ -- See -- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>. module Game.LambdaHack.Common.ServerCmd- ( CmdSer(..), CmdSerTakeTime(..), aidCmdSer, aidCmdSerTakeTime+ ( CmdSer(..), CmdTakeTimeSer(..), aidCmdSer, aidCmdTakeTimeSer , FailureSer(..), showFailureSer ) where @@ -18,76 +18,84 @@ -- | Abstract syntax of server commands. data CmdSer =- TakeTimeSer !CmdSerTakeTime- | GameRestartSer !ActorId !Text- | GameExitSer !ActorId+ CmdTakeTimeSer !CmdTakeTimeSer+ | GameRestartSer !ActorId !Text !Int ![(Int, Text)]+ | GameExitSer !ActorId !Int | GameSaveSer !ActorId deriving (Show, Eq) -data CmdSerTakeTime =+data CmdTakeTimeSer = MoveSer !ActorId !Vector | MeleeSer !ActorId !ActorId | DisplaceSer !ActorId !ActorId | AlterSer !ActorId !Point !(Maybe F.Feature) | WaitSer !ActorId- | PickupSer !ActorId !ItemId !Int !InvChar- | DropSer !ActorId !ItemId+ | PickupSer !ActorId !ItemId !Int+ | DropSer !ActorId !ItemId !Int | ProjectSer !ActorId !Point !Int !ItemId !Container | ApplySer !ActorId !ItemId !Container | TriggerSer !ActorId !(Maybe F.Feature)- | SetPathSer !ActorId ![Vector]+ | SetTrajectorySer !ActorId deriving (Show, Eq) --- | The actor that start performing the command (may be dead, after+-- | The actor that starts performing the command (may be dead, after -- the command is performed). aidCmdSer :: CmdSer -> ActorId aidCmdSer cmd = case cmd of- TakeTimeSer cmd2 -> aidCmdSerTakeTime cmd2- GameRestartSer aid _ -> aid- GameExitSer aid -> aid+ CmdTakeTimeSer cmd2 -> aidCmdTakeTimeSer cmd2+ GameRestartSer aid _ _ _ -> aid+ GameExitSer aid _ -> aid GameSaveSer aid -> aid -aidCmdSerTakeTime :: CmdSerTakeTime -> ActorId-aidCmdSerTakeTime cmd = case cmd of+aidCmdTakeTimeSer :: CmdTakeTimeSer -> ActorId+aidCmdTakeTimeSer cmd = case cmd of MoveSer aid _ -> aid MeleeSer aid _ -> aid DisplaceSer aid _ -> aid AlterSer aid _ _ -> aid WaitSer aid -> aid- PickupSer aid _ _ _ -> aid- DropSer aid _ -> aid+ PickupSer aid _ _ -> aid+ DropSer aid _ _ -> aid ProjectSer aid _ _ _ _ -> aid ApplySer aid _ _ -> aid TriggerSer aid _ -> aid- SetPathSer aid _ -> aid+ SetTrajectorySer aid -> aid data FailureSer = MoveNothing+ | MeleeSelf | MeleeDistant | DisplaceDistant | DisplaceAccess+ | DisplaceProjectiles | AlterDistant | AlterBlockActor | AlterBlockItem | AlterNothing+ | PickupOverfull | ProjectAimOnself | ProjectBlockTerrain | ProjectBlockActor | ProjectBlockFoes+ | ProjectBlind | TriggerNothing showFailureSer :: FailureSer -> Msg showFailureSer failureSer = case failureSer of MoveNothing -> "wasting time on moving into obstacle"+ MeleeSelf -> "trying to melee oneself" MeleeDistant -> "trying to melee a distant foe" DisplaceDistant -> "trying to switch places with a distant actor" DisplaceAccess -> "switching places without access"+ DisplaceProjectiles -> "trying to switch places with multiple projectiles" AlterDistant -> "trying to alter a distant tile" AlterBlockActor -> "blocked by an actor" AlterBlockItem -> "jammed by an item" AlterNothing -> "wasting time on altering nothing"+ PickupOverfull -> "cannot carry any more" ProjectAimOnself -> "cannot aim at oneself" ProjectBlockTerrain -> "aiming obstructed by terrain" ProjectBlockActor -> "aiming blocked by an actor" ProjectBlockFoes -> "aiming interrupted by foes"+ ProjectBlind -> "blind actors cannot aim" TriggerNothing -> "wasting time on triggering nothing"
Game/LambdaHack/Common/State.hs view
@@ -8,7 +8,7 @@ , defStateGlobal, emptyState, localFromGlobal , updateDungeon, updateDepth, updateActorD, updateItemD , updateFaction, updateTime, updateCOps, getLocalTime- , isSpawnFaction, isSummonFaction+ , isSpawnFaction ) where import Data.Binary@@ -21,7 +21,7 @@ import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY+import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Time import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Utils.Frequency@@ -44,17 +44,18 @@ -- TODO: add a flag 'fresh' and when saving levels, don't save -- and when loading regenerate this level. unknownLevel :: Kind.Ops TileKind -> Int -> X -> Y- -> Text -> ([Point], [Point]) -> Int -> Int -> Int+ -> Text -> ([Point], [Point]) -> Int -> Int -> Int -> Bool -> Level unknownLevel Kind.Ops{ouniqGroup} ldepth lxsize lysize ldesc lstair lclear- lsecret lhidden =+ lsecret lhidden lescape = let unknownId = ouniqGroup "unknown space"+ outerId = ouniqGroup "basic outer fence" in Level { ldepth , lprio = EM.empty , lfloor = EM.empty- , ltile = unknownTileMap unknownId lxsize lysize- , lxsize = lxsize- , lysize = lysize+ , ltile = unknownTileMap unknownId outerId lxsize lysize+ , lxsize+ , lysize , lsmell = EM.empty , ldesc , lstair@@ -65,12 +66,18 @@ , litemFreq = toFreq "client item freq" [] , lsecret , lhidden+ , lescape } -unknownTileMap :: Kind.Id TileKind -> Int -> Int -> TileMap-unknownTileMap unknownId cxsize cysize =- let bounds = (origin, toPoint cxsize $ PointXY (cxsize - 1, cysize - 1))- in Kind.listArray bounds (repeat unknownId)+unknownTileMap :: Kind.Id TileKind -> Kind.Id TileKind -> Int -> Int -> TileMap+unknownTileMap unknownId outerId lxsize lysize =+ let unknownMap = PointArray.replicateA lxsize lysize unknownId+ borders = [ Point x y+ | x <- [0, lxsize - 1], y <- [1..lysize - 2] ]+ ++ [ Point x y+ | x <- [0..lxsize - 1], y <- [0, lysize - 1] ]+ outerUpdate = zip borders $ repeat outerId+ in unknownMap PointArray.// outerUpdate -- | Initial complete global game state. defStateGlobal :: Dungeon -> Int@@ -109,7 +116,7 @@ { _sdungeon = EM.map (\Level{..} -> unknownLevel cotile ldepth lxsize lysize ldesc lstair lclear- lsecret lhidden)+ lsecret lhidden lescape) _sdungeon , .. }@@ -148,11 +155,7 @@ -- | Tell whether the faction can spawn actors. isSpawnFaction :: FactionId -> State -> Bool-isSpawnFaction fid s = isSpawnFact (_scops s) $ _sfactionD s EM.! fid---- | Tell whether actors of the faction can be summoned by items, etc..-isSummonFaction :: FactionId -> State -> Bool-isSummonFaction fid s = isSummonFact (_scops s) $ _sfactionD s EM.! fid+isSpawnFaction fid s = isSpawnFact $ _sfactionD s EM.! fid sdungeon :: State -> Dungeon sdungeon = _sdungeon@@ -196,4 +199,4 @@ _stime <- get _shigh <- get let _scops = undefined -- overwritten by recreated cops- return State{..}+ return $! State{..}
Game/LambdaHack/Common/Tile.hs view
@@ -14,21 +14,22 @@ -- Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m). module Game.LambdaHack.Common.Tile ( SmellTime- , kindHasFeature, kindHas, hasFeature- , isClear, isLit, isExplorable, similar, speedup- , openTo, closeTo, revealAs, hideAs, openable, closable, changeable+ , kindHasFeature, hasFeature+ , isClear, isLit, isWalkable, isPassable, isDoor, isSuspect+ , isExplorable, lookSimilar, speedup+ , openTo, closeTo, causeEffects, revealAs, hideAs+ , isOpenable, isClosable, isChangeable, isEscape, isStair ) where -import qualified Data.Array.Unboxed as A-import qualified Data.List as L+import Control.Exception.Assert.Sugar import Data.Maybe +import qualified Game.LambdaHack.Common.Effect as Effect import qualified Game.LambdaHack.Common.Feature as F import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.Time import Game.LambdaHack.Content.TileKind-import Control.Exception.Assert.Sugar -- | The last time a hero left a smell in a given tile. To be used -- by monsters that hunt by smell.@@ -36,57 +37,101 @@ -- | Whether a tile kind has the given feature. kindHasFeature :: F.Feature -> TileKind -> Bool+{-# INLINE kindHasFeature #-} kindHasFeature f t = f `elem` tfeature t --- | Whether a tile kind has all features of the first set--- and no features of the second.-kindHas :: [F.Feature] -> [F.Feature] -> TileKind -> Bool-kindHas yes no t = L.all (`kindHasFeature` t) yes- && not (L.any (`kindHasFeature` t) no)- -- | Whether a tile kind (specified by its id) has the given feature. hasFeature :: Kind.Ops TileKind -> F.Feature -> Kind.Id TileKind -> Bool-hasFeature Kind.Ops{okind} f t =- kindHasFeature f (okind t)+{-# INLINE hasFeature #-}+hasFeature Kind.Ops{okind} f t = kindHasFeature f (okind t) -- | Whether a tile does not block vision. -- Essential for efficiency of "FOV", hence tabulated. isClear :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool-isClear Kind.Ops{ospeedup = Just Kind.TileSpeedup{isClearTab}} = isClearTab+{-# INLINE isClear #-}+isClear Kind.Ops{ospeedup = Just Kind.TileSpeedup{isClearTab}} =+ \k -> Kind.accessTab isClearTab k isClear cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile -- | Whether a tile is lit on its own. -- Essential for efficiency of "Perception", hence tabulated. isLit :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool-isLit Kind.Ops{ospeedup = Just Kind.TileSpeedup{isLitTab}} = isLitTab+{-# INLINE isLit #-}+isLit Kind.Ops{ospeedup = Just Kind.TileSpeedup{isLitTab}} =+ \k -> Kind.accessTab isLitTab k isLit cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile --- | Whether a tile can be explored, possibly yielding a treasure--- or a hidden message.+-- | Whether actors can walk into a tile.+-- Essential for efficiency of pathfinding, hence tabulated.+isWalkable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+{-# INLINE isWalkable #-}+isWalkable Kind.Ops{ospeedup = Just Kind.TileSpeedup{isWalkableTab}} =+ \k -> Kind.accessTab isWalkableTab k+isWalkable cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile++-- | Whether actors can walk into a tile, perhaps opening a door first.+-- Essential for efficiency of pathfinding, hence tabulated.+isPassable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+{-# INLINE isPassable #-}+isPassable Kind.Ops{ospeedup = Just Kind.TileSpeedup{isPassableTab}} =+ \k -> Kind.accessTab isPassableTab k+isPassable cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile++-- | Whether a tile is a door, open or closed.+-- Essential for efficiency of pathfinding, hence tabulated.+isDoor :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+{-# INLINE isDoor #-}+isDoor Kind.Ops{ospeedup = Just Kind.TileSpeedup{isDoorTab}} =+ \k -> Kind.accessTab isDoorTab k+isDoor cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile++-- | Whether a tile is suspect.+-- Essential for efficiency of pathfinding, hence tabulated.+isSuspect :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+{-# INLINE isSuspect #-}+isSuspect Kind.Ops{ospeedup = Just Kind.TileSpeedup{isSuspectTab}} =+ \k -> Kind.accessTab isSuspectTab k+isSuspect cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile++-- | Whether a tile can be explored, possibly yielding a treasure.+-- Note that non-walkable tiles can hold treasure, e.g., caches. isExplorable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool-isExplorable cops tk = isClear cops tk || hasFeature cops F.Walkable tk+{-# INLINE isExplorable #-}+isExplorable cotile t =+ isWalkable cotile t || isDoor cotile t || isChangeable cotile t -- | The player can't tell one tile from the other.-similar :: TileKind -> TileKind -> Bool-similar t u =+lookSimilar :: TileKind -> TileKind -> Bool+{-# INLINE lookSimilar #-}+lookSimilar t u = tsymbol t == tsymbol u && tname t == tname u && tcolor t == tcolor u && tcolor2 t == tcolor2 u speedup :: Bool -> Kind.Ops TileKind -> Kind.Speedup TileKind-speedup allClear Kind.Ops{ofoldrWithKey, obounds} =- let createTab :: (TileKind -> Bool) -> A.UArray (Kind.Id TileKind) Bool- createTab p =- let f _ k acc = p k : acc- clearAssocs = ofoldrWithKey f []- in A.listArray obounds clearAssocs- tabulate :: (TileKind -> Bool) -> Kind.Id TileKind -> Bool- tabulate p = (createTab p A.!)- isClearTab | allClear = tabulate $ not . kindHasFeature F.Impenetrable- | otherwise = tabulate $ kindHasFeature F.Clear- isLitTab = tabulate $ kindHasFeature F.Lit- in Kind.TileSpeedup {isClearTab, isLitTab}+speedup allClear cotile =+ -- Vectors pack bools as Word8 by default. No idea if the extra memory+ -- taken makes random lookups more or less efficient, so not optimizing+ -- further, until I have benchmarks.+ let isClearTab | allClear = Kind.createTab cotile+ $ not . kindHasFeature F.Impenetrable+ | otherwise = Kind.createTab cotile+ $ kindHasFeature F.Clear+ isLitTab = Kind.createTab cotile $ not . kindHasFeature F.Dark+ isWalkableTab = Kind.createTab cotile $ kindHasFeature F.Walkable+ isPassableTab = Kind.createTab cotile $ \tk ->+ let getTo F.OpenTo{} = True+ getTo F.Walkable = True+ getTo _ = False+ in any getTo $ tfeature tk+ isDoorTab = Kind.createTab cotile $ \tk ->+ let getTo F.OpenTo{} = True+ getTo F.CloseTo{} = True+ getTo _ = False+ in any getTo $ tfeature tk+ isSuspectTab = Kind.createTab cotile $ kindHasFeature F.Suspect+ in Kind.TileSpeedup {..} openTo :: Kind.Ops TileKind -> Kind.Id TileKind -> Rnd (Kind.Id TileKind) openTo Kind.Ops{okind, opick} t = do@@ -110,6 +155,12 @@ fmap (fromMaybe $ assert `failure` group) $ opick group (const True) +causeEffects :: Kind.Ops TileKind -> Kind.Id TileKind -> [Effect.Effect Int]+causeEffects Kind.Ops{okind} t = do+ let getTo (F.Cause eff) acc = eff : acc+ getTo _ acc = acc+ foldr getTo [] $ tfeature $ okind t+ revealAs :: Kind.Ops TileKind -> Kind.Id TileKind -> Rnd (Kind.Id TileKind) revealAs Kind.Ops{okind, opick} t = do let getTo (F.RevealAs group) acc = group : acc@@ -129,23 +180,33 @@ Nothing -> t Just group -> ouniqGroup group --- | Whether a tile kind (specified by its id) has a OpenTo feature.-openable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool-openable Kind.Ops{okind} t =+-- | Whether a tile kind (specified by its id) has an OpenTo feature.+isOpenable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+isOpenable Kind.Ops{okind} t = let getTo F.OpenTo{} = True getTo _ = False in any getTo $ tfeature $ okind t -- | Whether a tile kind (specified by its id) has a CloseTo feature.-closable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool-closable Kind.Ops{okind} t =+isClosable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+isClosable Kind.Ops{okind} t = let getTo F.CloseTo{} = True getTo _ = False in any getTo $ tfeature $ okind t -- | Whether a tile kind (specified by its id) has a ChangeTo feature.-changeable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool-changeable Kind.Ops{okind} t =+isChangeable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+isChangeable Kind.Ops{okind} t = let getTo F.ChangeTo{} = True getTo _ = False in any getTo $ tfeature $ okind t++isEscape :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+isEscape cotile t = let isEffectEscape Effect.Escape{} = True+ isEffectEscape _ = False+ in any isEffectEscape $ causeEffects cotile t++isStair :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+isStair cotile t = let isEffectAscend Effect.Ascend{} = True+ isEffectAscend _ = False+ in any isEffectAscend $ causeEffects cotile t
Game/LambdaHack/Common/Time.hs view
@@ -5,13 +5,15 @@ , timeAdd, timeFit, timeNegate, timeScale, timeEpsilon , timeToDigit , Speed, toSpeed, speedZero, speedNormal, speedScale, speedAdd, speedNegate- , ticksPerMeter, traveled, speedFromWeight, rangeFromSpeed+ , ticksPerMeter, speedFromWeight, rangeFromSpeed ) where import Data.Binary import qualified Data.Char as Char import Data.Int (Int64) +import Game.LambdaHack.Common.Misc+ -- | Game time in ticks. The time dimension. -- One tick is 1 microsecond (one millionth of a second), -- one turn is 0.5 s.@@ -100,7 +102,7 @@ put (Speed n) = put n get = fmap Speed get --- | Number of seconds in a kilo-second.+-- | Number of seconds in a mega-second. sInMs :: Int64 sInMs = 1000000 @@ -130,13 +132,7 @@ -- | The number of time ticks it takes to walk 1 meter at the given speed. ticksPerMeter :: Speed -> Time-ticksPerMeter (Speed v) = Time $ _ticksInSecond * sInMs `div` v---- | Distance in meters (so also in tiles, given the chess metric)--- traveled in a given time by a body with a given speed.-traveled :: Speed -> Time -> Int-traveled (Speed v) (Time t) =- fromIntegral $ v * t `div` (_ticksInSecond * sInMs)+ticksPerMeter (Speed v) = Time $ _ticksInSecond * sInMs `divUp` v -- | Calculate projectile speed from item weight in grams -- and speed bonus in percents.@@ -148,13 +144,16 @@ mpMs | w <= 500 = sInMs * 16 | w > 500 && w <= 2000 = sInMs * 16 * 1500 `div` (w + 1000) | otherwise = sInMs * (10000 - w) `div` 1000- in Speed $ max 0 $ mpMs * (100 + b) `div` 100+ in Speed $ max 1 $ mpMs * (100 + b) `div` 100 -- | Calculate maximum range in meters of a projectile from its speed. -- See <https://github.com/kosmikus/LambdaHack/wiki/Item-statistics>.--- With this formula, each projectile flies for exactly one second,+-- With this formula, each projectile flies for at most 1 second, -- that is 2 turns, and then drops to the ground.--- Dividing and multiplying by 2 ensures both turns of flight--- cover the same distance.+-- We round down to the nearest multiple of 2 (unless the speed+-- is very low), to ensure both turns of flight cover the same distance. rangeFromSpeed :: Speed -> Int-rangeFromSpeed (Speed v) = fromIntegral $ 2 * (v `div` (sInMs * 2))+rangeFromSpeed (Speed v) =+ fromIntegral $ if v >= 2 * sInMs+ then 2 * (v `div` (2 * sInMs))+ else v `div` sInMs
Game/LambdaHack/Common/Vector.hs view
@@ -1,131 +1,206 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Basic operations on 2D vectors represented in an efficient, -- but not unique, way. module Game.LambdaHack.Common.Vector- ( Vector, toVector, toDir, shift, shiftBounded, moves- , isUnit, euclidDistSq, diagonal, neg, towards, displacement- , displacePath, shiftPath+ ( Vector(..), isUnit, isDiagonal, neg, chessDistVector, euclidDistSqVector+ , moves, compassText, vicinity, vicinityCardinal+ , shift, shiftBounded, trajectoryToPath, displacement, pathToTrajectory+ , RadianAngle, rotate, towards+ , BfsDistance, MoveLegal(..), apartBfs+ , fillBfs, findPathBfs, accessBfs, posAimsPos ) where +import Control.Arrow (second)+import Control.Exception.Assert.Sugar import Data.Binary+import Data.Bits (Bits, complement, (.&.), (.|.))+import qualified Data.EnumMap.Strict as EM+import Data.Int (Int32)+import Data.List+import Data.Maybe+import qualified Data.Sequence as Seq+import Data.Text (Text) import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY-import Game.LambdaHack.Common.VectorXY-import Control.Exception.Assert.Sugar+import qualified Game.LambdaHack.Common.PointArray as PointArray --- | 2D vectors represented as offsets in the linear framebuffer--- indexed by 'Point'.------ A newtype is used to prevent mixing up the type with @Point@ itself.--- Note that the offset representations of a vector is usually not unique.--- E.g., for vectors of length 1 in the chessboard metric, used to denote--- geographical directions, the representations are pairwise distinct--- if and only if the level width and height are at least 3.-newtype Vector = Vector Int- deriving (Eq, Ord, Read)+-- | 2D vectors in cartesian representation. Coordinates grow to the right+-- and down, so that the (1, 1) vector points to the bottom-right corner+-- of the screen.+data Vector = Vector+ { vx :: !X+ , vy :: !Y+ }+ deriving (Eq, Ord, Show, Read) instance Binary Vector where- put (Vector dir) = put dir- get = fmap Vector get+ put = put . (fromIntegral :: Int -> Int32) . fromEnum+ get = fmap (toEnum . (fromIntegral :: Int32 -> Int)) get --- For debugging.-instance Show Vector where- show (Vector n) = show n+instance Enum Vector where+ fromEnum = fromEnumVector+ toEnum = toEnumVector --- | Converts a vector in cartesian representation into @Vector@.-toVector :: X -> VectorXY -> Vector-toVector lxsize (VectorXY (x, y)) =- Vector $ x + y * lxsize+-- | Maximal supported vector X and Y coordinates.+maxVectorDim :: Int+{-# INLINE maxVectorDim #-}+maxVectorDim = 2 ^ (maxLevelDimExponent - 1) - 1 -isUnitXY :: VectorXY -> Bool-isUnitXY v = chessDistXY v == 1+fromEnumVector :: Vector -> Int+{-# INLINE fromEnumVector #-}+fromEnumVector (Vector vx vy) = vx + vy * (2 ^ maxLevelDimExponent) +toEnumVector :: Int -> Vector+{-# INLINE toEnumVector #-}+toEnumVector n =+ let (y, x) = n `quotRem` (2 ^ maxLevelDimExponent)+ (vx, vy) = if x > maxVectorDim+ then (x - 2 ^ maxLevelDimExponent, y + 1)+ else if x < - maxVectorDim+ then (x + 2 ^ maxLevelDimExponent, y - 1)+ else (x, y)+ in Vector{..}+ -- | Tells if a vector has length 1 in the chessboard metric.-isUnit :: X -> Vector -> Bool-isUnit lxsize = isUnitXY . fromDir lxsize+isUnit :: Vector -> Bool+{-# INLINE isUnit #-}+isUnit v = chessDistVector v == 1 --- | Converts a unit vector in cartesian representation into @Vector@.-toDir :: X -> VectorXY -> Vector-toDir lxsize v@(VectorXY (x, y)) =- assert (lxsize >= 3 && isUnitXY v `blame` "ambiguous XY vector conversion"- `twith` (lxsize, v)) $- Vector $ x + y * lxsize+-- | Checks whether a unit vector is a diagonal direction,+-- as opposed to cardinal. If the vector is not unit,+-- it checks that the vector is not horizontal nor vertical.+isDiagonal :: Vector -> Bool+{-# INLINE isDiagonal #-}+isDiagonal (Vector x y) = x * y /= 0 --- | Converts a unit vector in the offset representation--- into the cartesian representation. Arbitrary vectors can't be--- converted uniquely.-fromDir :: X -> Vector -> VectorXY-fromDir lxsize (Vector dir) =- assert (lxsize >= 3 && isUnitXY res &&- fst len1 + snd len1 * lxsize == dir- `blame` "ambiguous vector conversion" `twith` (lxsize, dir, res))- res- where- (x, y) = (dir `mod` lxsize, dir `div` lxsize)- -- Pick the vector's canonical form of length 1:- len1 = if x > 1- then (x - lxsize, y + 1)- else (x, y)- res = VectorXY len1+-- | Reverse an arbirary vector.+neg :: Vector -> Vector+{-# INLINE neg #-}+neg (Vector vx vy) = Vector (-vx) (-vy) +-- | Squared euclidean distance between two vectors.+euclidDistSqVector :: Vector -> Vector -> Int+{-# INLINE euclidDistSqVector #-}+euclidDistSqVector (Vector x0 y0) (Vector x1 y1) =+ let square n = n ^ (2 :: Int)+ in square (x1 - x0) + square (y1 - y0)++-- | The lenght of a vector in the chessboard metric,+-- where diagonal moves cost 1.+chessDistVector :: Vector -> Int+{-# INLINE chessDistVector #-}+chessDistVector (Vector x y) = max (abs x) (abs y)++-- | Vectors of all unit moves in the chessboard metric,+-- clockwise, starting north-west.+moves :: [Vector]+moves =+ map (uncurry Vector)+ [(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)]++moveTexts :: [Text]+moveTexts = ["NW", "N", "NE", "E", "SE", "S", "SW", "W"]++compassText :: Vector -> Text+compassText v = let m = EM.fromList $ zip moves moveTexts+ in fromMaybe (assert `failure` "not a unit vector"+ `twith` v) $ EM.lookup v m++-- | Vectors of all cardinal direction unit moves, clockwise, starting north.+movesCardinal :: [Vector]+movesCardinal = map (uncurry Vector) [(0, -1), (1, 0), (0, 1), (-1, 0)]++-- | All (8 at most) closest neighbours of a point within an area.+vicinity :: X -> Y -- ^ limit the search to this area+ -> Point -- ^ position to find neighbours of+ -> [Point]+vicinity lxsize lysize p =+ [ res | dxy <- moves+ , let res = shift p dxy+ , inside res (0, 0, lxsize - 1, lysize - 1) ]++-- | All (4 at most) cardinal direction neighbours of a point within an area.+vicinityCardinal :: X -> Y -- ^ limit the search to this area+ -> Point -- ^ position to find neighbours of+ -> [Point]+vicinityCardinal lxsize lysize p =+ [ res | dxy <- movesCardinal+ , let res = shift p dxy+ , inside res (0, 0, lxsize - 1, lysize - 1) ]+ -- | Translate a point by a vector.------ Particularly simple and fast implementation in the linear representation. shift :: Point -> Vector -> Point-shift p (Vector dir) = toEnum $ fromEnum p + dir+{-# INLINE shift #-}+shift (Point x0 y0) (Vector x1 y1) = Point (x0 + x1) (y0 + y1) -- | Translate a point by a vector, but only if the result fits in an area.-shiftBounded :: X -> (X, Y, X, Y) -> Point -> Vector -> Point-shiftBounded lxsize area pos dir =- let res = shift pos dir- in if inside lxsize res area then res else pos+shiftBounded :: X -> Y -> Point -> Vector -> Point+shiftBounded lxsize lysize pos v@(Vector xv yv) =+ if inside pos (-xv, -yv, lxsize - xv - 1, lysize - yv - 1)+ then shift pos v+ else pos --- | Vectors of all unit moves, clockwise, starting north-west.-moves :: X -> [Vector]-moves lxsize = map (toDir lxsize) movesXY+-- | A list of points that a list of vectors leads to.+trajectoryToPath :: Point -> [Vector] -> [Point]+trajectoryToPath _ [] = []+trajectoryToPath start (v : vs) = let next = shift start v+ in next : trajectoryToPath next vs --- | Squared euclidean distance between two unit vectors.-euclidDistSq :: X -> Vector -> Vector -> Int-euclidDistSq lxsize dir0 dir1- | VectorXY (x0, y0) <- fromDir lxsize dir0- , VectorXY (x1, y1) <- fromDir lxsize dir1 =- euclidDistSqXY $ VectorXY (x1 - x0, y1 - y0)+-- | A vector from a point to another. We have+--+-- > shift pos1 (displacement pos1 pos2) == pos2+displacement :: Point -> Point -> Vector+{-# INLINE displacement #-}+displacement (Point x0 y0) (Point x1 y1) = Vector (x1 - x0) (y1 - y0) --- | Checks whether a unit vector is a diagonal direction,--- as opposed to cardinal. If the vector is not unit,--- it reject horizontal and vertical vectors.-diagonal :: X -> Vector -> Bool-diagonal lxsize dir | VectorXY (x, y) <- fromDir lxsize dir =- x * y /= 0+-- | A list of vectors between a list of points.+pathToTrajectory :: [Point] -> [Vector]+pathToTrajectory [] = []+pathToTrajectory lp1@(_ : lp2) = zipWith displacement lp1 lp2 --- | Reverse an arbirary vector.-neg :: Vector -> Vector-neg (Vector dir) = Vector (-dir)+type RadianAngle = Double +-- | Rotate a vector by the given angle (expressed in radians)+-- counterclockwise and return a unit vector approximately in the resulting+-- direction.+rotate :: RadianAngle -> Vector -> Vector+rotate angle (Vector x' y') =+ let x = fromIntegral x'+ y = fromIntegral y'+ -- Minus before the angle comes from our coordinates being+ -- mirrored along the X axis (Y coordinates grow going downwards).+ dx = x * cos (-angle) - y * sin (-angle)+ dy = x * sin (-angle) + y * cos (-angle)+ in normalize dx dy+ -- TODO: use bla for that -- | Given a vector of arbitrary non-zero length, produce a unit vector -- that points in the same direction (in the chessboard metric). -- Of several equally good directions it picks one of those that visually -- (in the euclidean metric) maximally align with the original vector.-normalize :: X -> VectorXY -> Vector-normalize lxsize v@(VectorXY (dx, dy)) =+normalize :: Double -> Double -> Vector+normalize dx dy = assert (dx /= 0 || dy /= 0 `blame` "can't normalize zero" `twith` (dx, dy)) $ let angle :: Double- angle = atan (fromIntegral dy / fromIntegral dx) / (pi / 2)+ angle = atan (dy / dx) / (pi / 2) dxy | angle <= -0.75 && angle >= -1.25 = (0, -1) | angle <= -0.25 = (1, -1) | angle <= 0.25 = (1, 0) | angle <= 0.75 = (1, 1) | angle <= 1.25 = (0, 1) | otherwise = assert `failure` "impossible angle"- `twith` (lxsize, dx, dy, angle)- rxy = if dx >= 0- then VectorXY dxy- else negXY $ VectorXY dxy- in assert (not (isUnitXY v) || v == rxy+ `twith` (dx, dy, angle)+ in if dx >= 0+ then uncurry Vector dxy+ else neg $ uncurry Vector dxy++normalizeVector :: Vector -> Vector+normalizeVector v@(Vector vx vy) =+ let res = normalize (fromIntegral vx) (fromIntegral vy)+ in assert (not (isUnit v) || v == res `blame` "unit vector gets untrivially normalized"- `twith` (v, rxy))- $ toDir lxsize rxy+ `twith` (v, res))+ res -- TODO: Perhaps produce all acceptable directions and let AI choose. -- That would also eliminate the Doubles. Or only directions from bla?@@ -137,28 +212,130 @@ -- (in the chessboard metric) it picks one of those that visually -- (in the euclidean metric) maximally align with the vector between -- the two points.-towards :: X -> Point -> Point -> Vector-towards lxsize pos0 pos1 =- assert (pos0 /= pos1 `blame` "towards self" `twith` (pos0, pos1)) $- let v = displacementXYZ lxsize pos0 pos1- in normalize lxsize v+towards :: Point -> Point -> Vector+towards pos0 pos1 =+ assert (pos0 /= pos1 `blame` "towards self" `twith` (pos0, pos1))+ $ normalizeVector $ displacement pos0 pos1 --- | A vector from a point to another. We have------ > shift pos1 (displacement pos1 pos2) == pos2------ Particularly simple and fast implementation in the linear representation.-displacement :: Point -> Point -> Vector-displacement pos1 pos2 = Vector $ fromEnum pos2 - fromEnum pos1+newtype BfsDistance = BfsDistance Word8+ deriving (Show, Eq, Ord, Enum, Bounded, Bits) --- | A list of vectors between a list of points.-displacePath :: [Point] -> [Vector]-displacePath [] = []-displacePath lp1@(_ : lp2) = zipWith displacement lp1 lp2+data MoveLegal = MoveBlocked | MoveToOpen | MoveToUnknown+ deriving Eq --- | A list of points that a list of vectors leads to.-shiftPath :: Point -> [Vector] -> [Point]-shiftPath _ [] = []-shiftPath start (v : vs) =- let next = shift start v- in next : shiftPath next vs+minKnownBfs :: BfsDistance+minKnownBfs = toEnum $ (1 + fromEnum (maxBound :: BfsDistance)) `div` 2++apartBfs :: BfsDistance+apartBfs = pred minKnownBfs++-- TODO: Move somewhere; in particular, only clients need to know that.+fillBfs :: (Point -> Point -> MoveLegal) -- ^ is move from a known tile legal+ -> (Point -> Point -> Bool) -- ^ is a move from unknown legal+ -> Point -- ^ starting position+ -> PointArray.Array BfsDistance -- ^ initial array, with @apartBfs@+ -> PointArray.Array BfsDistance -- ^ array with calculated distances+fillBfs isEnterable passUnknown origin aInitial =+ -- TODO: copy, thaw, mutate, freeze+ let maxUnknownBfs = pred apartBfs+ maxKnownBfs = pred maxBound+ bfs :: Seq.Seq (Point, BfsDistance)+ -> PointArray.Array BfsDistance+ -> PointArray.Array BfsDistance+ bfs q a =+ case Seq.viewr q of+ Seq.EmptyR -> a -- no more positions to check+ _ Seq.:> (_, d)+ | d == maxUnknownBfs || d == maxKnownBfs -> a -- too far+ q1 Seq.:> (pos, oldDistance) | oldDistance >= minKnownBfs ->+ let distance = succ oldDistance+ allMvs = map (shift pos) moves+ freshMv p = a PointArray.! p == apartBfs+ freshMvs = filter freshMv allMvs+ legal p = (p, isEnterable pos p)+ legalities = map legal freshMvs+ notBlocked = filter ((/= MoveBlocked) . snd) legalities+ legalToDist l = if l == MoveToOpen+ then distance+ else distance .&. complement minKnownBfs+ mvs = map (second legalToDist) notBlocked+ q2 = foldr (Seq.<|) q1 mvs+ s2 = a PointArray.// mvs+ in bfs q2 s2+ q1 Seq.:> (pos, oldDistance) ->+ let distance = succ oldDistance+ allMvs = map (shift pos) moves+ goodMv p = a PointArray.! p == apartBfs && passUnknown pos p+ mvs = zip (filter goodMv allMvs) (repeat distance)+ q2 = foldr (Seq.<|) q1 mvs+ s2 = a PointArray.// mvs+ in bfs q2 s2+ origin0 = (origin, minKnownBfs)+ in bfs (Seq.singleton origin0) (aInitial PointArray.// [origin0])++-- TODO: Use http://harablog.wordpress.com/2011/09/07/jump-point-search/+-- to determine a few really different paths and compare them,+-- e.g., how many closed doors they pass, open doors, unknown tiles+-- on the path or close enough to reveal them.+-- Also, check if JPS can somehow optimize BFS or pathBfs.+-- | Find a path, without the source position, with the smallest length.+-- The @eps@ coefficient determines which direction (or the closest+-- directions available) that path should prefer, where 0 means north-west+-- and 1 means north.+findPathBfs :: (Point -> Point -> MoveLegal)+ -> (Point -> Point -> Bool)+ -> Point -> Point -> Int -> PointArray.Array BfsDistance+ -> Maybe [Point]+findPathBfs isEnterable passUnknown source target sepsRaw bfs =+ assert (bfs PointArray.! source == minKnownBfs) $+ let targetDist = bfs PointArray.! target+ in if targetDist == apartBfs+ then Nothing+ else+ let eps = abs sepsRaw `mod` length moves+ mix (x : xs) ys = x : mix ys xs+ mix [] ys = ys+ preferedMoves = let (ch1, ch2) = splitAt eps moves+ ch = ch2 ++ ch1+ in mix ch (reverse ch)+ track :: Point -> BfsDistance -> [Point] -> [Point]+ track pos oldDist suffix | oldDist == minKnownBfs =+ assert (pos == source+ `blame` (source, target, pos, suffix)) suffix+ track pos oldDist suffix | oldDist > minKnownBfs =+ let dist = pred oldDist+ children = map (shift pos) preferedMoves+ matchesDist p = bfs PointArray.! p == dist+ && isEnterable p pos == MoveToOpen+ minP = fromMaybe (assert `failure` (pos, oldDist, children))+ (find matchesDist children)+ in track minP dist (pos : suffix)+ track pos oldDist suffix =+ let distUnknown = pred oldDist+ distKnown = distUnknown .|. minKnownBfs+ children = map (shift pos) preferedMoves+ matchesDistUnknown p = bfs PointArray.! p == distUnknown+ && passUnknown p pos+ matchesDistKnown p = bfs PointArray.! p == distKnown+ && isEnterable p pos == MoveToUnknown+ (minP, dist) = case find matchesDistKnown children of+ Just p -> (p, distKnown)+ Nothing -> case find matchesDistUnknown children of+ Just p -> (p, distUnknown)+ Nothing -> assert `failure` (pos, oldDist, children)+ in track minP dist (pos : suffix)+ in Just $ track target targetDist []++accessBfs :: PointArray.Array BfsDistance -> Point -> Maybe Int+{-# INLINE accessBfs #-}+accessBfs bfs target =+ let dist = bfs PointArray.! target+ in if dist == apartBfs+ then Nothing+ else Just $ fromEnum $ dist .&. complement minKnownBfs++posAimsPos :: PointArray.Array BfsDistance -> Point -> Point -> Bool+{-# INLINE posAimsPos #-}+posAimsPos bfs bpos target =+ let mdist = accessBfs bfs target+ in mdist == Just (chessDist bpos target)
− Game/LambdaHack/Common/VectorXY.hs
@@ -1,57 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}--- | Basic cartesian geometry operations on 2D vectors.-module Game.LambdaHack.Common.VectorXY- ( VectorXY(..), shiftXY, movesXY, movesCardinalXY- , chessDistXY, euclidDistSqXY, negXY, vicinityXY, vicinityCardinalXY- ) where--import Data.Binary--import Game.LambdaHack.Common.PointXY---- | 2D vectors in cartesian representation.-newtype VectorXY = VectorXY (X, Y)- deriving (Eq, Ord, Show, Read, Binary)---- | Shift a point by a vector.-shiftXY :: PointXY -> VectorXY -> PointXY-shiftXY (PointXY (x0, y0)) (VectorXY (x1, y1)) = PointXY (x0 + x1, y0 + y1)---- | Vectors of all unit moves in the chessboard metric,--- clockwise, starting north-west.-movesXY :: [VectorXY]-movesXY =- map VectorXY- [(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)]---- | Vectors of all cardinal direction unit moves, clockwise, starting north.-movesCardinalXY :: [VectorXY]-movesCardinalXY = map VectorXY [(0, -1), (1, 0), (0, 1), (-1, 0)]---- | The lenght of a vector in the chessboard metric,--- where diagonal moves cost 1.-chessDistXY :: VectorXY -> Int-chessDistXY (VectorXY (x, y)) = max (abs x) (abs y)---- | Squared euclidean length of a vector.-euclidDistSqXY :: VectorXY -> Int-euclidDistSqXY (VectorXY (x, y)) = x * x + y * y---- | Reverse an arbirary vector.-negXY :: VectorXY -> VectorXY-negXY (VectorXY (x, y)) = VectorXY (-x, -y)---- | All (8 at most) closest neighbours of a point within an area.-vicinityXY :: (X, Y, X, Y) -- ^ limit the search to this area- -> PointXY -- ^ position to find neighbours of- -> [PointXY]-vicinityXY area xy =- [ res | dxy <- movesXY, let res = shiftXY xy dxy, insideXY res area ]---- | All (4 at most) cardinal direction neighbours of a point within an area.-vicinityCardinalXY :: (X, Y, X, Y) -- ^ limit the search to this area- -> PointXY -- ^ position to find neighbours of- -> [PointXY]-vicinityCardinalXY area xy =- [ res- | dxy <- movesCardinalXY, let res = shiftXY xy dxy, insideXY res area ]
Game/LambdaHack/Content/ActorKind.hs view
@@ -1,12 +1,13 @@ -- | The type of kinds of monsters and heroes. module Game.LambdaHack.Content.ActorKind- ( ActorKind(..), avalidate+ ( ActorKind(..), validateActorKind ) where import Control.Arrow ((&&&)) import Data.List import qualified Data.Ord as Ord import Data.Text (Text)+import qualified Data.Text as T import Game.LambdaHack.Common.Ability import Game.LambdaHack.Common.Color@@ -35,10 +36,12 @@ -- | Filter a list of kinds, passing through only the incorrect ones, if any. -- -- Make sure actor kinds can be told apart on the level map.-avalidate :: [ActorKind] -> [ActorKind]-avalidate l =+validateActorKind :: [ActorKind] -> [ActorKind]+validateActorKind l = let cmp = Ord.comparing $ asymbol &&& acolor eq ka1 ka2 = cmp ka1 ka2 == Ord.EQ sorted = sortBy cmp l nubbed = nubBy eq sorted- in deleteFirstsBy eq sorted nubbed+ tooSimilar = deleteFirstsBy eq sorted nubbed+ tooVerbose = filter (\ak -> T.length (aname ak) > 25) l+ in tooSimilar ++ tooVerbose
Game/LambdaHack/Content/CaveKind.hs view
@@ -1,14 +1,13 @@ -- | The type of cave layout kinds. module Game.LambdaHack.Content.CaveKind- ( CaveKind(..), cvalidate+ ( CaveKind(..), validateCaveKind ) where -import qualified Data.List as L import Data.Text (Text) import qualified Data.Text as T import Game.LambdaHack.Common.Misc-import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Random -- | Parameters for the generation of dungeon levels.@@ -33,10 +32,11 @@ , citemFreq :: ![(Int, Text)] -- ^ item groups to consider , cdefTile :: !Text -- ^ the default cave tile group name , cdarkCorTile :: !Text -- ^ the dark cave corridor tile group name- , clitCorTile :: !Text -- ^ the dark cave corridor tile group name+ , clitCorTile :: !Text -- ^ the lit 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+ , couterFenceTile :: !Text -- ^ the outer fence wall group name+ , clegendDarkTile :: !Text -- ^ the dark place plan legend group name+ , clegendLitTile :: !Text -- ^ the lit place plan legend group name } deriving Show -- No Eq and Ord to make extending it logically sound, see #53 @@ -45,18 +45,21 @@ -- -- Catch caves with not enough space for all the places. Check the size -- of the cave descriptions to make sure they fit on screen.-cvalidate :: [CaveKind] -> [CaveKind]-cvalidate = L.filter (\ CaveKind{ cgrid- , cminPlaceSize- , cmaxPlaceSize- , ..- } ->+validateCaveKind :: [CaveKind] -> [CaveKind]+validateCaveKind = filter (\ CaveKind{..} -> let (maxGridX, maxGridY) = maxDiceXY cgrid (minMinSizeX, minMinSizeY) = minDiceXY cminPlaceSize (maxMinSizeX, maxMinSizeY) = maxDiceXY cminPlaceSize (minMaxSizeX, minMaxSizeY) = minDiceXY cmaxPlaceSize- xborder = if maxGridX == 1 then 3 else 1- yborder = if maxGridX == 1 then 3 else 1+ -- If there is at most one room, we need extra borders for a passage,+ -- but if there may be more rooms, we have that space, anyway,+ -- because multiple rooms take more space than borders.+ xborder = if maxGridX == 1 || couterFenceTile /= "basic outer fence"+ then 2+ else 0+ yborder = if maxGridY == 1 || couterFenceTile /= "basic outer fence"+ then 2+ else 0 in T.length cname > 25 || cxsize < 7 || cysize < 7@@ -64,5 +67,5 @@ || minMinSizeY < 1 || minMaxSizeX < maxMinSizeX || minMaxSizeY < maxMinSizeY- || maxGridX * (maxMinSizeX + xborder) >= cxsize- || maxGridY * (maxMinSizeY + yborder) >= cysize)+ || maxGridX * (maxMinSizeX + 1) + xborder >= cxsize+ || maxGridY * (maxMinSizeY + 1) + yborder >= cysize)
Game/LambdaHack/Content/FactionKind.hs view
@@ -1,6 +1,6 @@ -- | The type of kinds of game factions (heroes, enemies, NPCs, etc.). module Game.LambdaHack.Content.FactionKind- ( FactionKind(..), fvalidate+ ( FactionKind(..), validateFactionKind ) where import Data.Text (Text)@@ -12,12 +12,12 @@ { fsymbol :: !Char -- ^ a symbol , fname :: !Text -- ^ short description , ffreq :: !Freqs -- ^ frequency within groups- , fAbilityLeader :: ![Ability] -- ^ abilities of the selected actor+ , fAbilityLeader :: ![Ability] -- ^ abilities of the picked leader , fAbilityOther :: ![Ability] -- ^ abilities of the other actors } deriving Show -- | No specific possible problems for the content of this kind, so far, -- so the validation function always returns the empty list of offending kinds.-fvalidate :: [FactionKind] -> [FactionKind]-fvalidate _ = []+validateFactionKind :: [FactionKind] -> [FactionKind]+validateFactionKind _ = []
Game/LambdaHack/Content/ItemKind.hs view
@@ -1,32 +1,33 @@ -- | The type of kinds of weapons and treasure. module Game.LambdaHack.Content.ItemKind- ( ItemKind(..), ivalidate+ ( ItemKind(..), validateItemKind ) where import Data.Text (Text)+import qualified Data.Text as T import qualified NLP.Miniutter.English as MU -import Game.LambdaHack.Common.Effect import Game.LambdaHack.Common.Flavour+import qualified Game.LambdaHack.Common.ItemFeature as IF import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Random -- | Item properties that are fixed for a given kind of items. data ItemKind = ItemKind- { isymbol :: !Char -- ^ map symbol- , iname :: !Text -- ^ generic name- , ifreq :: !Freqs -- ^ frequency within groups- , iflavour :: ![Flavour] -- ^ possible flavours- , ieffect :: !(Effect RollDeep) -- ^ the effect when activated- , icount :: !RollDeep -- ^ created in that quantify- , iverbApply :: !MU.Part -- ^ the verb for applying and combat- , iverbProject :: !MU.Part -- ^ 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+ , icount :: !RollDeep -- ^ created in that quantify+ , iverbApply :: !MU.Part -- ^ the verb for applying and combat+ , iverbProject :: !MU.Part -- ^ the verb for projecting+ , iweight :: !Int -- ^ weight in grams+ , itoThrow :: !Int -- ^ percentage bonus to throw speed+ , ifeature :: ![IF.Feature] -- ^ properties } deriving Show -- No Eq and Ord to make extending it logically sound, see #53 -- | No specific possible problems for the content of this kind, so far, -- so the validation function always returns the empty list of offending kinds.-ivalidate :: [ItemKind] -> [ItemKind]-ivalidate _ = []+validateItemKind :: [ItemKind] -> [ItemKind]+validateItemKind l = filter (\ik -> T.length (iname ik) > 25) l
Game/LambdaHack/Content/ModeKind.hs view
@@ -1,12 +1,12 @@ -- | The type of kinds of game modes. module Game.LambdaHack.Content.ModeKind- ( Caves, Players(..), Player(..), ModeKind(..), mvalidate+ ( Caves, Players(..), Player(..), ModeKind(..), validateModeKind ) where import Data.Binary import qualified Data.EnumMap.Strict as EM import Data.Text (Text)-import NLP.Miniutter.English ()+import qualified NLP.Miniutter.English as MU () import Game.LambdaHack.Common.Misc (Freqs, LevelId) @@ -39,6 +39,7 @@ data Player = Player { playerName :: !Text -- ^ name of the player , playerFaction :: !Text -- ^ name of faction(s) the player can control+ , playerSpawn :: !Int -- ^ spawning frequency , playerEntry :: !LevelId -- ^ level where the initial members start , playerInitial :: !Int -- ^ number of initial members , playerAiLeader :: !Bool -- ^ is the leader under AI control?@@ -50,17 +51,19 @@ } deriving (Show, Eq) +-- TODO: assert every Player's playerName's first word's length <= 15 -- TODO: assert if no UI, both Ai are on and there are some non-spawners; -- assert that playersEnemy and playersAlly mention only factions in play. -- | No specific possible problems for the content of this kind, so far, -- so the validation function always returns the empty list of offending kinds.-mvalidate :: [ModeKind] -> [ModeKind]-mvalidate _ = []+validateModeKind :: [ModeKind] -> [ModeKind]+validateModeKind _ = [] instance Binary Player where put Player{..} = do put playerName put playerFaction+ put playerSpawn put playerEntry put playerInitial put playerAiLeader@@ -70,10 +73,11 @@ get = do playerName <- get playerFaction <- get+ playerSpawn <- get playerEntry <- get playerInitial <- get playerAiLeader <- get playerAiOther <- get playerHuman <- get playerUI <- get- return Player{..}+ return $! Player{..}
Game/LambdaHack/Content/PlaceKind.hs view
@@ -1,9 +1,8 @@ -- | The type of kinds of rooms, halls and passages. module Game.LambdaHack.Content.PlaceKind- ( PlaceKind(..), Cover(..), Fence(..), pvalidate+ ( PlaceKind(..), Cover(..), Fence(..), validatePlaceKind ) where -import qualified Data.List as L import Data.Text (Text) import qualified Data.Text as T @@ -43,9 +42,9 @@ -- | Filter a list of kinds, passing through only the incorrect ones, if any. -- -- Verify that the top-left corner map is rectangular and not empty.-pvalidate :: [PlaceKind] -> [PlaceKind]-pvalidate = L.filter (\ PlaceKind{..} ->+validatePlaceKind :: [PlaceKind] -> [PlaceKind]+validatePlaceKind = filter (\ PlaceKind{..} -> let dxcorner = case ptopLeft of [] -> 0 l : _ -> T.length l- in dxcorner /= 0 && L.any (/= dxcorner) (L.map T.length ptopLeft))+ in dxcorner /= 0 && any (/= dxcorner) (map T.length ptopLeft))
Game/LambdaHack/Content/RuleKind.hs view
@@ -1,17 +1,21 @@ -- | The type of game rule sets and assorted game data. module Game.LambdaHack.Content.RuleKind- ( RuleKind(..), ruvalidate+ ( RuleKind(..), validateRuleKind, FovMode(..) ) where +import Data.Binary import Data.Text (Text) import Data.Version +import Game.LambdaHack.Common.HumanCmd+import qualified Game.LambdaHack.Common.Key as K import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY-import Game.LambdaHack.Content.TileKind -- TODO: very few rules are configurable yet, extend as needed.+-- TODO: in the future, in @raccessible@ check flying for chasms,+-- swimming for water, etc.+-- TODO: tweak other code to allow games with only cardinal direction moves -- | The type of game rule sets and assorted game data. --@@ -23,25 +27,64 @@ -- and then @RuleKind@ will become just a starting template, analogously -- as for the other content. ----- The @raccessible@ field holds a predicate that tells--- whether one position is accessible from another.+-- The @raccessible@ field hold extra conditions that have to be met+-- for a tile to be accessible, on top of being an open tile+-- (or openable, in some contexts). The @raccessibleDoor@ field+-- contains yet additional conditions concerning tiles that are doors,+-- whether open or closed. -- Precondition: the two positions are next to each other.+-- We assume the predicate is symmetric. data RuleKind = RuleKind- { 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- , ritemRanged :: ![Char] -- ^ symbols of ranged weapons and missiles- , 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+ { rsymbol :: !Char -- ^ a symbol+ , rname :: !Text -- ^ short description+ , rfreq :: !Freqs -- ^ frequency within groups+ , raccessible :: !(Maybe (Point -> Point -> Bool))+ , raccessibleDoor :: !(Maybe (Point -> Point -> 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+ , ritemRanged :: ![Char] -- ^ ranged weapons and missiles+ , ritemProject :: ![Char] -- ^ symbols of items AI can project+ , rcfgUIName :: !FilePath -- ^ base name of the UI config file+ , rcfgUIDefault :: !String -- ^ the default UI settings config file+ , rmainMenuArt :: !Text -- ^ the ASCII art for the Main Menu+ , rhumanCommands :: ![(K.KM, (CmdCategory, HumanCmd))]+ -- ^ default client commands+ , rfirstDeathEnds :: !Bool -- ^ whether first non-spawner actor death+ -- ends the game+ , rfovMode :: !FovMode -- ^ FOV calculation mode+ , rsaveBkpClips :: !Int -- ^ game backup is saved that often+ , rleadLevelClips :: !Int -- ^ AI/spawn leader level flipped that often+ , rscoresFile :: !FilePath -- ^ name of the scores file+ , rsavePrefix :: !String -- ^ name of the savefile prefix } +-- 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)++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)"+ -- | A dummy instance of the 'Show' class, to satisfy general requirments -- about content. We won't have many rule sets and they contain functions, -- so defining a proper instance is not practical.@@ -49,5 +92,5 @@ show _ = "The game ruleset specification." -- | Validates the ASCII art format (TODO).-ruvalidate :: [RuleKind] -> [RuleKind]-ruvalidate _ = []+validateRuleKind :: [RuleKind] -> [RuleKind]+validateRuleKind _ = []
Game/LambdaHack/Content/TileKind.hs view
@@ -1,27 +1,28 @@ -- | The type of kinds of terrain tiles. module Game.LambdaHack.Content.TileKind- ( TileKind(..), tvalidate+ ( TileKind(..), validateTileKind, actionFeatures ) where -import qualified Data.List as L+import Control.Exception.Assert.Sugar import qualified Data.Map.Strict as M+import Data.Maybe+import qualified Data.Set as S import Data.Text (Text) import Game.LambdaHack.Common.Color-import Game.LambdaHack.Common.Feature+import qualified Game.LambdaHack.Common.Feature as F import Game.LambdaHack.Common.Misc-import Control.Exception.Assert.Sugar -- | The type of kinds of terrain tiles. See @Tile.hs@ for explanation -- of the absence of a corresponding type @Tile@ that would hold -- particular concrete tiles in the dungeon. data TileKind = TileKind- { tsymbol :: !Char -- ^ map symbol- , tname :: !Text -- ^ short description- , tfreq :: !Freqs -- ^ frequency within groups- , tcolor :: !Color -- ^ map color- , tcolor2 :: !Color -- ^ map color when not in FOV- , tfeature :: ![Feature] -- ^ properties+ { tsymbol :: !Char -- ^ map symbol+ , tname :: !Text -- ^ short description+ , tfreq :: !Freqs -- ^ frequency within groups+ , tcolor :: !Color -- ^ map color+ , tcolor2 :: !Color -- ^ map color when not in FOV+ , tfeature :: ![F.Feature] -- ^ properties } deriving Show -- No Eq and Ord to make extending it logically sound, see #53 @@ -30,25 +31,52 @@ -- TODO: verify that OpenTo, CloseTo and ChangeTo are assigned as specified. -- | Filter a list of kinds, passing through only the incorrect ones, if any. ----- If tiles look the same on the map, the description should be the same, too.--- Otherwise, the player has to inspect manually all the tiles of that kind--- to see if any is special. This is a part of a stronger--- but less precise property that tiles that look the same can't be--- distinguished by player actions (but may behave differently--- wrt dungeon generation, AI preferences, etc.).-tvalidate :: [TileKind] -> [TileKind]-tvalidate lt =- let listFov f = L.map (\ kt -> ( ( tsymbol kt- , Suspect `elem` tfeature kt- , f kt- )- , [kt] )) lt+-- If tiles look the same on the map, the description and the substantial+-- features should be the same, too. Otherwise, the player has to inspect+-- manually all the tiles of that kind, or even experiment with them,+-- to see if any is special. This would be tedious. Note that iiles may freely+-- differ wrt dungeon generation, AI preferences, etc.+validateTileKind :: [TileKind] -> [TileKind]+validateTileKind lt =+ let listFov f = map (\kt -> ( ( tsymbol kt+ , F.Suspect `elem` tfeature kt+ , f kt+ )+ , [kt] )) lt mapFov :: (TileKind -> Color) -> M.Map (Char, Bool, Color) [TileKind] mapFov f = M.fromListWith (++) $ listFov f namesUnequal [] = assert `failure` "no TileKind content" `twith` lt- namesUnequal (hd : tl) = -- Check that at least one is different.- L.any (/= tname hd) (L.map tname tl)- confusions f = L.filter namesUnequal $ M.elems $ mapFov f+ namesUnequal (hd : tl) =+ -- Catch if at least one is different.+ any (/= tname hd) (map tname tl)+ || any (/= actionFeatures True hd) (map (actionFeatures True) tl)+ confusions f = filter namesUnequal $ M.elems $ mapFov f in case confusions tcolor ++ confusions tcolor2 of [] -> [] l : _ -> l++-- | Features of tiles that differentiate them substantially from one another.+-- By tile contents validation condition, this means the player+-- can tell such tile apart, and only looking at the map, not tile name.+-- So if running uses this function, it won't stop at places that the player+-- can't himself tell from other places, and so running does not confer+-- any advantages, except UI convenience.+actionFeatures :: Bool -> TileKind -> S.Set F.Feature+actionFeatures markSuspect t =+ let f feat = case feat of+ F.Cause{} -> Just feat+ F.OpenTo{} -> Just $ F.OpenTo "" -- if needed, remove prefix/suffix+ F.CloseTo{} -> Just $ F.CloseTo ""+ F.ChangeTo{} -> Just $ F.ChangeTo ""+ F.Walkable -> Just feat+ F.Clear -> Just feat+ F.Suspect -> if markSuspect then Just feat else Nothing+ F.Aura{} -> Just feat+ F.Impenetrable -> Just feat+ F.Trail -> Just feat -- doesn't affect tile behaviour, but important+ F.HideAs{} -> Nothing+ F.RevealAs{} -> Nothing+ F.Dark -> Nothing -- not important any longer, after FOV computed+ F.CanItem -> Nothing+ F.CanActor -> Nothing+ in S.fromList $ mapMaybe f $ tfeature t
Game/LambdaHack/Frontend.hs view
@@ -12,6 +12,7 @@ import Control.Concurrent import Control.Concurrent.STM (TQueue, atomically, newTQueueIO, writeTQueue) import qualified Control.Concurrent.STM as STM+import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM import Data.Maybe@@ -21,7 +22,6 @@ import System.IO import System.IO.Unsafe (unsafePerformIO) -import Control.Exception.Assert.Sugar import Game.LambdaHack.Common.Animation import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Key as K@@ -59,7 +59,9 @@ startupF :: DebugModeCli -> IO () -> IO () startupF dbg cont =- (if sfrontendStd dbg then stdStartup else chosenStartup) dbg $ \fs -> do+ (if sfrontendNo dbg then noStartup+ else if sfrontendStd dbg then stdStartup+ else chosenStartup) dbg $ \fs -> do let debugPrint t = when (sdbgMsgCli dbg) $ do T.hPutStrLn stderr t hFlush stderr@@ -80,7 +82,7 @@ promptGetKey fs keys@(firstKM:_) frame = do km <- fpromptGetKey fs frame if km `elem` keys- then return km+ then return $! km else do let DebugModeCli{snoMore} = fdebugCli fs if snoMore then return firstKM@@ -92,7 +94,7 @@ connMulti = unsafePerformIO $ do fromMulti <- newMVar undefined toMulti <- newTQueueIO- return ConnMulti{..}+ return $! ConnMulti{..} -- | Augment a function that takes and returns keys. getConfirmGeneric :: Monad m@@ -109,7 +111,7 @@ let queue = toListLQueue $ fromMaybe newLQueue $ EM.lookup fid reqMap reqMap2 = EM.delete fid reqMap mapM_ (displayAc fs) queue- return reqMap2+ return $! reqMap2 displayAc :: Frontend -> AcFrame -> IO () displayAc fs (AcConfirm fr) = void $ getConfirmGeneric (promptGetKey fs) [] fr@@ -133,12 +135,12 @@ let topRight = True lxsize = xsizeSingleFrame frame lysize = ysizeSingleFrame frame- msg = "Player" <+> showT (fromEnum side) <> ","+ msg = "Player" <+> tshow (fromEnum side) <> "," <+> pname <> (if T.null pname then "" else ",") <+> "get ready!" animMap <- rndToIO $ fadeout out topRight lxsize lysize- let sfTop = truncateMsg lxsize msg- basicFrame = frame {sfTop}+ let sfTop = truncateToOverlay lxsize msg+ basicFrame = frame {sfTop} -- overwrite the whole original overlay animFrs = renderAnim lxsize lysize basicFrame animMap frs | out = animFrs -- Empty frame to mark the fade-in end,@@ -182,7 +184,7 @@ let singles = toSingles oldFid reqMap -- not @reqMap2@! lastFrame = fromMaybe oldFrame $ listToMaybe $ reverse singles fadeF fs True fid pname lastFrame- return reqMap2+ return $! reqMap2 let singles = toSingles fid reqMap2 firstFrame = fromMaybe frontFr $ listToMaybe singles -- TODO: @nU@ is unreliable, when some of UI players die;
Game/LambdaHack/Frontend/Chosen.hs view
@@ -2,7 +2,7 @@ -- | Re-export the operations of the chosen raw frontend -- (determined at compile time with cabal flags). module Game.LambdaHack.Frontend.Chosen- ( Frontend(..), chosenStartup, stdStartup+ ( Frontend(..), chosenStartup, stdStartup, noStartup , frontendName ) where @@ -44,5 +44,13 @@ cont $ Frontend { fdisplay = Std.fdisplay fs , fpromptGetKey = Std.fpromptGetKey fs+ , fdebugCli+ }++noStartup :: DebugModeCli -> (Frontend -> IO ()) -> IO ()+noStartup fdebugCli cont =+ cont $ Frontend+ { fdisplay = \_ _ -> return ()+ , fpromptGetKey = \_ -> return K.escKey , fdebugCli }
Game/LambdaHack/Frontend/Curses.hs view
@@ -1,4 +1,6 @@--- | Text frontend based on HSCurses.+-- | Text frontend based on HSCurses. This frontend is not fully supported+-- due to the limitations of the curses library (keys, colours, last character+-- of the last line). module Game.LambdaHack.Frontend.Curses ( -- * Session data type for the frontend FrontendSession@@ -8,16 +10,15 @@ , frontendName, startup ) where +import Control.Exception.Assert.Sugar import Control.Monad import Data.Char (chr, ord)-import qualified Data.List as L import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified UI.HSCurses.Curses as C import qualified UI.HSCurses.CursesHelper as C -import Control.Exception.Assert.Sugar-import Game.LambdaHack.Common.Animation (DebugModeCli (..), SingleFrame (..))+import Game.LambdaHack.Common.Animation import qualified Game.LambdaHack.Common.Color as Color import qualified Game.LambdaHack.Common.Key as K import Game.LambdaHack.Common.Msg@@ -45,7 +46,7 @@ -- No more color combinations possible: 16*4, 64 is max. bg <- Color.legalBG ] nr <- C.colorPairs- when (nr < L.length s) $+ when (nr < length s) $ C.end >> (assert `failure` "terminal has too few color pairs" `twith` nr) let (ks, vs) = unzip s ws <- C.convertStyles vs@@ -60,18 +61,19 @@ -> Maybe SingleFrame -- ^ the screen frame to draw -> IO () fdisplay _ _ Nothing = return ()-fdisplay FrontendSession{..} _ (Just SingleFrame{..}) = do+fdisplay FrontendSession{..} _ (Just rawSF) = do+ let SingleFrame{sfLevel} = overlayOverlay rawSF -- let defaultStyle = C.defaultCursesStyle -- Terminals with white background require this: let defaultStyle = sstyles M.! Color.defAttr C.erase C.setStyle defaultStyle- 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 $ T.unpack sfBottom)- let nm = L.zip [0..] $ L.map (L.zip [0..]) sfLevel+ let sfLevelDecoded = map decodeLine sfLevel+ level = init sfLevelDecoded ++ [init $ last sfLevelDecoded]+ nm = zip [0..] $ map (zip [0..]) level sequence_ [ C.setStyle (M.findWithDefault defaultStyle acAttr sstyles) >> C.mvWAddStr swin (y + 1) x [acChar] | (y, line) <- nm, (x, Color.AttrChar{..}) <- line ]@@ -100,6 +102,7 @@ C.KeyChar ' ' -> (K.Space, K.NoModifier) C.KeyChar '\t' -> (K.Tab, K.NoModifier) C.KeyBTab -> (K.BackTab, K.NoModifier)+ C.KeyBackspace -> (K.BackSpace, K.NoModifier) C.KeyUp -> (K.Up, K.NoModifier) C.KeyDown -> (K.Down, K.NoModifier) C.KeyLeft -> (K.Left, K.NoModifier)@@ -107,8 +110,8 @@ C.KeyRight -> (K.Right, K.NoModifier) C.KeySRight -> (K.Right, K.NoModifier) C.KeyHome -> (K.Home, K.NoModifier)- C.KeyPPage -> (K.PgUp, K.NoModifier) C.KeyEnd -> (K.End, K.NoModifier)+ C.KeyPPage -> (K.PgUp, K.NoModifier) C.KeyNPage -> (K.PgDn, K.NoModifier) C.KeyBeg -> (K.Begin, K.NoModifier) C.KeyB2 -> (K.Begin, K.NoModifier)@@ -122,12 +125,12 @@ | ord '\^A' <= ord c && ord c <= ord '\^Z' -> -- Alas, only lower-case letters. (K.Char $ chr $ ord c - ord '\^A' + ord 'a', K.Control)- -- Movement keys are more important than hero selection,+ -- Movement keys are more important than leader picking, -- so disabling the latter and interpreting the keypad numbers -- as movement: | c `elem` ['1'..'9'] -> (K.KP c, K.NoModifier) | otherwise -> (K.Char c, K.NoModifier)- _ -> (K.Unknown (showT e), K.NoModifier)+ _ -> (K.Unknown (tshow e), K.NoModifier) toFColor :: Color.Color -> C.ForegroundColor toFColor Color.Black = C.BlackF
Game/LambdaHack/Frontend/Gtk.hs view
@@ -10,6 +10,7 @@ ) where import Control.Concurrent+import Control.Exception.Assert.Sugar import Control.Monad import Control.Monad.Reader import qualified Data.ByteString.Char8 as BS@@ -17,13 +18,10 @@ import Data.List import qualified Data.Map.Strict as M import Data.Maybe-import qualified Data.Text as T-import Data.Text.Encoding (encodeUtf8) import Graphics.UI.Gtk hiding (Point) import System.Time -import Control.Exception.Assert.Sugar-import Game.LambdaHack.Common.Animation (DebugModeCli (..), SingleFrame (..))+import Game.LambdaHack.Common.Animation import qualified Game.LambdaHack.Common.Color as Color import qualified Game.LambdaHack.Common.Key as K import Game.LambdaHack.Utils.LQueue@@ -75,13 +73,6 @@ _ -> putMVar sframeState fs -lengthQueue :: FrontendSession -> IO Int-lengthQueue FrontendSession{sframeState} = do- fs <- readMVar sframeState- case fs of- FPushed{..} -> return $ lengthLQueue fpushed- _ -> return 0- -- | The name of the frontend. frontendName :: String frontendName = "gtk"@@ -135,20 +126,9 @@ let !key = K.keyTranslate n !modifier = modifierTranslate mods liftIO $ do- unless (deadKey n) $ do- len <- lengthQueue sess- if n == "space" && len > 1 then- -- Only drop frames up to the first empty frame.- -- Some new frames may be arriving at the same time- -- or being displayed and removed, but we don't care.- onQueue dropStartLQueue sess- else- -- Store the key in the channel. All but last frame will be dropped- -- as soon as the channel is read. Use SPACE repeatedly to step- -- through some intermediate frames of an animation --- other keys- -- are not meant to be pressed many times before the engine- -- is ready to recognize and process them.- writeChan schanKey K.KM {key, modifier}+ unless (deadKey n) $+ -- Store the key in the channel.+ writeChan schanKey K.KM{key, modifier} return True -- Set the font specified in config, if any. f <- fontDescriptionFromString $ fromMaybe "" sfont@@ -202,7 +182,7 @@ setTo :: TextBuffer -> TextTag -> Int -> (Int, [TextTag]) -> IO () setTo _ _ _ (_, []) = return () setTo tb defAttr lx (ly, attr:attrs) = do- ib <- textBufferGetIterAtLineOffset tb (ly + 1) lx+ ib <- textBufferGetIterAtLineOffset tb ly lx ie <- textIterCopy ib let setIter :: TextTag -> Int -> [TextTag] -> IO () setIter previous repetitions [] = do@@ -250,8 +230,8 @@ -- Check if the time is up. let maxFps = fromMaybe defaultMaxFps smaxFps curTime <- getClockTime- let diffT = diffTime setTime curTime- if diffT > microInSec `div` maxPolls maxFps+ let diffSetCur = diffTime setTime curTime+ if diffSetCur > microInSec `div` maxPolls maxFps then do -- Delay half of the time difference. threadDelay $ diffTime curTime setTime `div` 2@@ -344,12 +324,13 @@ Just f -> putMVar slastFull (f, noDelay) evalFrame :: FrontendSession -> SingleFrame -> GtkFrame-evalFrame FrontendSession{stags} SingleFrame{..} =- let levelChar = map (T.pack . map Color.acChar) sfLevel- gfChar = encodeUtf8 $ T.intercalate (T.singleton '\n')- $ sfTop : levelChar ++ [sfBottom]- -- Strict version of @map (map ((stags M.!) . fst)) sfLevel@.- gfAttr = reverse $ foldl' ff [] sfLevel+evalFrame FrontendSession{stags} rawSF =+ let SingleFrame{sfLevel} = overlayOverlay rawSF+ sfLevelDecoded = map decodeLine sfLevel+ levelChar = unlines $ map (map Color.acChar) sfLevelDecoded+ gfChar = BS.pack $ init levelChar+ -- Strict version of @map (map ((stags M.!) . fst)) sfLevelDecoded@.+ gfAttr = reverse $ foldl' ff [] sfLevelDecoded ff ll l = reverse (foldl' f [] l) : ll f l ac = let !tag = stags M.! Color.acAttr ac in tag : l in GtkFrame{..}@@ -411,22 +392,31 @@ return () -- | Display a prompt, wait for any key.--- Starts in Push mode, ends in None mode.+-- Starts in Push mode, ends in Push or None mode. -- Syncs with the drawing threads by showing the last or all queued frames. fpromptGetKey :: FrontendSession -> SingleFrame -> IO K.KM fpromptGetKey sess@FrontendSession{sdebugCli=DebugModeCli{snoMore}, ..} frame = do pushFrame sess True True $ Just frame if snoMore then do- -- Show all frames synchronously.+ -- Show all frames synchronously. Keys completely ignored. TODO: K.Space fs <- takeMVar sframeState displayAllFramesSync sess fs putMVar sframeState FNone return K.escKey else do km <- readChan schanKey- -- Show the last frame and empty the queue.- trimFrameState sess+ case km of+ K.KM{key=K.Space, modifier=K.NoModifier} ->+ -- Drop frames up to the first empty frame.+ -- Keep the last non-empty frame, if any.+ -- Pressing SPACE repeatedly can be used to step+ -- through intermediate stages of an animation,+ -- whereas any other key skips the whole animation outright.+ onQueue dropStartLQueue sess+ _ ->+ -- Show the last non-empty frame and empty the queue.+ trimFrameState sess return km -- | Tells a dead key.
Game/LambdaHack/Frontend/Std.hs view
@@ -10,11 +10,9 @@ import qualified Data.ByteString.Char8 as BS import Data.Char (chr, ord)-import qualified Data.List as L-import Data.Text.Encoding (encodeUtf8) import qualified System.IO as SIO -import Game.LambdaHack.Common.Animation (DebugModeCli (..), SingleFrame (..))+import Game.LambdaHack.Common.Animation import qualified Game.LambdaHack.Common.Color as Color import qualified Game.LambdaHack.Common.Key as K @@ -37,9 +35,9 @@ -> Maybe SingleFrame -- ^ the screen frame to draw -> IO () fdisplay _ _ Nothing = return ()-fdisplay _ _ (Just SingleFrame{..}) =- let chars = L.map (BS.pack . L.map Color.acChar) sfLevel- bs = [encodeUtf8 sfTop, BS.empty] ++ chars ++ [encodeUtf8 sfBottom, BS.empty]+fdisplay _ _ (Just rawSF) =+ let SingleFrame{sfLevel} = overlayOverlay rawSF+ bs = map (BS.pack . map Color.acChar . decodeLine) sfLevel ++ [BS.empty] in mapM_ BS.putStrLn bs -- | Input key via the frontend.@@ -70,7 +68,7 @@ c | ord '\^A' <= ord c && ord c <= ord '\^Z' -> -- Alas, only lower-case letters. (K.Char $ chr $ ord c - ord '\^A' + ord 'a', K.Control)- -- Movement keys are more important than hero selection,+ -- Movement keys are more important than leader picking, -- so disabling the latter and interpreting the keypad numbers -- as movement: | c `elem` ['1'..'9'] -> (K.KP c, K.NoModifier)
Game/LambdaHack/Frontend/Vty.hs view
@@ -8,12 +8,10 @@ , frontendName, startup ) where -import qualified Data.List as L-import Data.Text.Encoding (encodeUtf8) import Graphics.Vty import qualified Graphics.Vty as Vty -import Game.LambdaHack.Common.Animation (DebugModeCli (..), SingleFrame (..))+import Game.LambdaHack.Common.Animation import qualified Game.LambdaHack.Common.Color as Color import qualified Game.LambdaHack.Common.Key as K import Game.LambdaHack.Common.Msg@@ -42,16 +40,14 @@ -> Maybe SingleFrame -- ^ the screen frame to draw -> IO () fdisplay _ _ Nothing = return ()-fdisplay FrontendSession{svty} _ (Just SingleFrame{..}) =- let img = (foldr (<->) empty_image- . L.map (foldr (<|>) empty_image- . L.map (\ Color.AttrChar{..} ->+fdisplay FrontendSession{svty} _ (Just rawSF) =+ let SingleFrame{sfLevel} = overlayOverlay rawSF+ img = (foldr (<->) empty_image+ . map (foldr (<|>) empty_image+ . map (\ Color.AttrChar{..} -> char (setAttr acAttr) acChar)))- sfLevel- pic = pic_for_image $- utf8_bytestring (setAttr Color.defAttr) (encodeUtf8 sfTop)- <-> img <->- utf8_bytestring (setAttr Color.defAttr) (encodeUtf8 sfBottom)+ $ map decodeLine sfLevel+ pic = pic_for_image img in update svty pic -- | Input key via the frontend.@@ -64,7 +60,7 @@ EvKey n mods -> do let key = keyTranslate n modifier = modifierTranslate mods- return K.KM {key, modifier}+ return $! K.KM {key, modifier} _ -> nextEvent sess -- | Display a prompt, wait for any key.@@ -73,6 +69,9 @@ fdisplay sess True $ Just frame nextEvent sess +-- TODO: Ctrl-Home and Ctrl-End are the same as Home and End on some terminals+-- so we should probably go back to using 0-9 (and Shift) for movement+-- but let's wait until vty 5.0 is out and see if it helps. keyTranslate :: Key -> K.Key keyTranslate n = case n of@@ -81,23 +80,26 @@ (KASCII ' ') -> K.Space (KASCII '\t') -> K.Tab KBackTab -> K.BackTab+ KBS -> K.BackSpace KUp -> K.Up KDown -> K.Down KLeft -> K.Left KRight -> K.Right KHome -> K.Home- KPageUp -> K.PgUp KEnd -> K.End+ KPageUp -> K.PgUp KPageDown -> K.PgDn KBegin -> K.Begin+ KNP5 -> K.Begin (KASCII c) -> K.Char c- _ -> K.Unknown (showT n)+ _ -> K.Unknown (tshow n) -- | Translates modifiers to our own encoding. modifierTranslate :: [Modifier] -> K.Modifier modifierTranslate mods = if MCtrl `elem` mods then K.Control else K.NoModifier +-- TODO: with vty 5.0 check if bold is still needed. -- A hack to get bright colors via the bold attribute. Depending on terminal -- settings this is needed or not and the characters really get bold or not. -- HSCurses does this by default, but in Vty you have to request the hack.
Game/LambdaHack/Server.hs view
@@ -16,9 +16,9 @@ import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.ServerCmd+import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Frontend import Game.LambdaHack.Server.Action-import Game.LambdaHack.Server.Fov import Game.LambdaHack.Server.LoopAction import Game.LambdaHack.Server.ServerSem import Game.LambdaHack.Server.State@@ -28,49 +28,54 @@ -- indicates if the command took some time. cmdSerSem :: (MonadAtomic m, MonadServer m) => CmdSer -> m Bool cmdSerSem cmd = case cmd of- TakeTimeSer cmd2 -> cmdSerSemTakeTime cmd2 >> return True- GameRestartSer aid t -> gameRestartSer aid t >> return False- GameExitSer aid -> gameExitSer aid >> return False+ CmdTakeTimeSer cmd2 -> cmdSerSemTakeTime cmd2 >> return True+ GameRestartSer aid t d names -> gameRestartSer aid t d names >> return False+ GameExitSer aid d -> gameExitSer aid d >> return False GameSaveSer _ -> gameSaveSer >> return False -cmdSerSemTakeTime :: (MonadAtomic m, MonadServer m) => CmdSerTakeTime -> m ()+cmdSerSemTakeTime :: (MonadAtomic m, MonadServer m) => CmdTakeTimeSer -> m () cmdSerSemTakeTime cmd = case cmd of MoveSer source target -> moveSer source target MeleeSer source target -> meleeSer source target DisplaceSer source target -> displaceSer source target AlterSer source tpos mfeat -> alterSer source tpos mfeat WaitSer aid -> waitSer aid- PickupSer aid i k l -> pickupSer aid i k l- DropSer aid iid -> dropSer aid iid+ PickupSer aid i k -> pickupSer aid i k+ DropSer aid iid k -> dropSer aid iid k ProjectSer aid p eps iid container -> projectSer aid p eps iid container ApplySer aid iid container -> applySer aid iid container TriggerSer aid mfeat -> triggerSer aid mfeat- SetPathSer aid path -> setPathSer aid path+ SetTrajectorySer aid -> setTrajectorySer aid debugArgs :: IO DebugModeSer debugArgs = do args <- getArgs let usage = [ "Configure debug options here, gameplay options in config.rules.ini."- , " --knowMap reveal map for all clients in the next game"- , " --knowEvents show all events in the next game (needs --knowMap)"- , " --sniffIn display all incoming commands on console "- , " --sniffOut display all outgoing commands on console "- , " --allClear let all map tiles be translucent"- , " --gameMode m start next game in the given mode"- , " --newGame start a new game, overwriting the save file"- , " --stopAfter n exit this game session after around n seconds"- , " --dumpConfig dump server config at the start of the game"- , " --dbgMsgSer let the server emit its internal debug messages"- , " --font fn use the given font for the main game window"- , " --maxFps n display at most n frames per second"- , " --noDelay don't maintain any requested delays between frames"- , " --noMore auto-answer all prompts"- , " --noAnim don't show any animations"- , " --savePrefix prepend the text to all savefile names"- , " --frontendStd use the simple stdout/stdin frontend"- , " --dbgMsgCli let clients emit their internal debug messages"- , " --fovMode m set a Field of View mode, where m can be"+ , " --knowMap reveal map for all clients in the next game"+ , " --knowEvents show all events in the next game (needs --knowMap)"+ , " --sniffIn display all incoming commands on console "+ , " --sniffOut display all outgoing commands on console "+ , " --allClear let all map tiles be translucent"+ , " --gameMode m start next game in the given mode"+ , " --newGame start a new game, overwriting the save file"+ , " --difficulty n set difficulty for all UI players to n"+ , " --stopAfter n exit this game session after around n seconds"+ , " --benchmark print stats, limit saving and other file operations"+ , " --setDungeonRng s set dungeon generation RNG seed to string s"+ , " --setMainRng s set the main game RNG seed to string s"+ , " --dumpInitRngs dump RNG states from the start of the game"+ , " --dbgMsgSer let the server emit its internal debug messages"+ , " --font fn use the given font for the main game window"+ , " --maxFps n display at most n frames per second"+ , " --noDelay don't maintain any requested delays between frames"+ , " --noMore auto-answer all prompts"+ , " --noAnim don't show any animations"+ , " --savePrefix prepend the text to all savefile names"+ , " --frontendStd use the simple stdout/stdin frontend"+ , " --frontendNo use no frontend at all (for AIvsAI benchmarks)"+ , " --dbgMsgCli let clients emit their internal debug messages"+ , " --fovMode m set a Field of View mode, where m can be" , " Digital r, r > 0" , " Permissive" , " Shadow"@@ -92,12 +97,22 @@ parseArgs ("--newGame" : rest) = let debugSer = parseArgs rest in debugSer { snewGameSer = True- , sdebugCli =- (sdebugCli debugSer) {snewGameCli = True}}+ , sdebugCli = (sdebugCli debugSer) {snewGameCli = True}}+ parseArgs ("--difficulty" : s : rest) =+ let debugSer = parseArgs rest+ diff = 5 - read s+ in debugSer { sdifficultySer = diff+ , sdebugCli = (sdebugCli debugSer) {sdifficultyCli = diff}} parseArgs ("--stopAfter" : s : rest) = (parseArgs rest) {sstopAfter = Just $ read s}- parseArgs ("--dumpConfig" : rest) =- (parseArgs rest) {sdumpConfig = True}+ parseArgs ("--benchmark" : rest) =+ (parseArgs rest) {sbenchmark = True}+ parseArgs ("--setDungeonRng" : s : rest) =+ (parseArgs rest) {sdungeonRng = Just $ read s}+ parseArgs ("--setMainRng" : s : rest) =+ (parseArgs rest) {smainRng = Just $ read s}+ parseArgs ("--dumpInitRngs" : rest) =+ (parseArgs rest) {sdumpInitRngs = True} parseArgs ("--fovMode" : "Digital" : r : rest) | (read r :: Int) > 0 = (parseArgs rest) {sfovMode = Just $ Digital $ read r} parseArgs ("--fovMode" : mode : rest) =@@ -124,10 +139,13 @@ let debugSer = parseArgs rest in debugSer { ssavePrefixSer = Just s , sdebugCli =- (sdebugCli debugSer) {ssavePrefixCli = Just s}}+ (sdebugCli debugSer) {ssavePrefixCli = Just s}} parseArgs ("--frontendStd" : rest) = let debugSer = parseArgs rest in debugSer {sdebugCli = (sdebugCli debugSer) {sfrontendStd = True}}+ parseArgs ("--frontendNo" : rest) =+ let debugSer = parseArgs rest+ in debugSer {sdebugCli = (sdebugCli debugSer) {sfrontendNo = True}} parseArgs ("--dbgMsgCli" : rest) = let debugSer = parseArgs rest in debugSer {sdebugCli = (sdebugCli debugSer) {sdbgMsgCli = True}}@@ -146,7 +164,7 @@ -> (Kind.COps -> DebugModeCli -> ((FactionId -> ChanFrontend -> ChanServer CmdClientUI CmdSer -> IO ())- -> (FactionId -> ChanServer CmdClientAI CmdSerTakeTime+ -> (FactionId -> ChanServer CmdClientAI CmdTakeTimeSer -> IO ()) -> IO ()) -> IO ())
Game/LambdaHack/Server/Action.hs view
@@ -11,9 +11,10 @@ , sendUpdateAI, sendQueryAI, sendPingAI , sendUpdateUI, sendQueryUI, sendPingUI -- * Assorted primitives- , debugPrint, dumpCfg- , mkConfigRules, restoreScore, revealItems, deduceQuits- , rndToAction, resetSessionStart, elapsedSessionTimeGT+ , debugPrint, dumpRngs+ , getSetGen, restoreScore, revealItems, deduceQuits+ , rndToAction, resetSessionStart, resetGameStart, elapsedSessionTimeGT+ , tellAllClipPS, tellGameClipPS , resetFidPerception, getPerFid , childrenServer ) where@@ -21,7 +22,8 @@ import Control.Concurrent import Control.Concurrent.STM (TQueue, atomically) import qualified Control.Concurrent.STM as STM-import Control.DeepSeq+import qualified Control.Exception as Ex hiding (handle)+import Control.Exception.Assert.Sugar import Control.Monad import qualified Control.Monad.State as St import qualified Data.EnumMap.Strict as EM@@ -39,29 +41,29 @@ import qualified System.Random as R import System.Time -import Control.Exception.Assert.Sugar import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.AtomicCmd import Game.LambdaHack.Common.ClientCmd-import qualified Game.LambdaHack.Common.ConfigIO as ConfigIO import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.HighScore as HighScore import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Random+import Game.LambdaHack.Common.Save import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.ServerCmd import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Time import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Frontend as Frontend import Game.LambdaHack.Server.Action.ActionClass-import Game.LambdaHack.Server.Config import Game.LambdaHack.Server.Fov import Game.LambdaHack.Server.State import Game.LambdaHack.Utils.File@@ -81,9 +83,9 @@ cops <- getsState scops lvl <- getLevel lid fovMode <- getsServer $ sfovMode . sdebugSer- s <- getState- let per = levelPerception cops s (fromMaybe (Digital 12) fovMode) fid lid lvl- upd = EM.adjust (EM.adjust (const per) lid) fid+ per <- getsState+ $ levelPerception cops (fromMaybe (Digital 12) fovMode) fid lid lvl+ let upd = EM.adjust (EM.adjust (const per) lid) fid modifyServer $ \ser -> ser {sper = upd (sper ser)} getPerFid :: MonadServer m => FactionId -> LevelId -> m Perception@@ -95,14 +97,13 @@ `twith` (lid, fid)) $ EM.lookup lid fper return $! per --- | Dumps the current game rules configuration to a file.-dumpCfg :: MonadServer m => m String-dumpCfg = do- Config{configAppDataDir, configRulesCfgFile} <- getsServer sconfig- let fn = configAppDataDir </> configRulesCfgFile ++ ".dump"- config <- getsServer sconfig- liftIO $ ConfigIO.dump config fn- return fn+-- | Dumps RNG states from the start of the game to a file.+dumpRngs :: MonadServer m => m ()+dumpRngs = do+ dataDir <- liftIO appDataDir+ let fn = dataDir </> "rngs.dump"+ rngs <- getsServer srngs+ liftIO $ writeFile fn (show rngs) writeTQueueAI :: MonadConnServer m => CmdClientAI -> TQueue CmdClientAI -> m () writeTQueueAI cmd fromServer = do@@ -120,15 +121,15 @@ liftIO $ T.hPutStrLn stderr d liftIO $ atomically $ STM.writeTQueue fromServer cmd -readTQueueAI :: MonadConnServer m => TQueue CmdSerTakeTime -> m CmdSerTakeTime+readTQueueAI :: MonadConnServer m => TQueue CmdTakeTimeSer -> m CmdTakeTimeSer readTQueueAI toServer = do cmd <- liftIO $ atomically $ STM.readTQueue toServer debug <- getsServer $ sniffIn . sdebugSer when debug $ do- let aid = aidCmdSerTakeTime cmd- d <- debugAid aid "CmdSerTakeTime" cmd+ let aid = aidCmdTakeTimeSer cmd+ d <- debugAid aid "CmdTakeTimeSer" cmd liftIO $ T.hPutStrLn stderr d- return cmd+ return $! cmd readTQueueUI :: MonadConnServer m => TQueue CmdSer -> m CmdSer readTQueueUI toServer = do@@ -138,14 +139,14 @@ let aid = aidCmdSer cmd d <- debugAid aid "CmdSer" cmd liftIO $ T.hPutStrLn stderr d- return cmd+ return $! cmd sendUpdateAI :: MonadConnServer m => FactionId -> CmdClientAI -> m () sendUpdateAI fid cmd = do conn <- getsDict $ snd . (EM.! fid) writeTQueueAI cmd $ fromServer conn -sendQueryAI :: MonadConnServer m => FactionId -> ActorId -> m CmdSerTakeTime+sendQueryAI :: MonadConnServer m => FactionId -> ActorId -> m CmdTakeTimeSer sendQueryAI fid aid = do conn <- getsDict $ snd . (EM.! fid) writeTQueueAI (CmdQueryAI aid) $ fromServer conn@@ -155,9 +156,9 @@ sendPingAI fid = do conn <- getsDict $ snd . (EM.! fid) writeTQueueAI CmdPingAI $ fromServer conn- -- debugPrint $ "AI client" <+> showT fid <+> "pinged..."+ -- debugPrint $ "AI client" <+> tshow fid <+> "pinged..." cmdHack <- readTQueueAI $ toServer conn- -- debugPrint $ "AI client" <+> showT fid <+> "responded."+ -- debugPrint $ "AI client" <+> tshow fid <+> "responded." assert (cmdHack == WaitSer (toEnum (-1))) skip sendUpdateUI :: MonadConnServer m => FactionId -> CmdClientUI -> m ()@@ -184,28 +185,41 @@ Nothing -> assert `failure` "no channel for faction" `twith` fid Just (_, conn) -> do writeTQueueUI CmdPingUI $ fromServer conn- -- debugPrint $ "UI client" <+> showT fid <+> "pinged..."+ -- debugPrint $ "UI client" <+> tshow fid <+> "pinged..." cmdHack <- readTQueueUI $ toServer conn- -- debugPrint $ "UI client" <+> showT fid <+> "responded."- assert (cmdHack == TakeTimeSer (WaitSer (toEnum (-1)))) skip+ -- debugPrint $ "UI client" <+> tshow fid <+> "responded."+ assert (cmdHack == CmdTakeTimeSer (WaitSer (toEnum (-1)))) skip +-- TODO: refactor wrt Game.LambdaHack.Common.Save -- | Read the high scores table. Return the empty table if no file.--- Warning: when it's used, the game state--- may still be undefined, hence the config is given as an argument.-restoreScore :: MonadServer m => Config -> m HighScore.ScoreTable-restoreScore Config{configAppDataDir, configScoresFile} = do- let path = configAppDataDir </> configScoresFile+restoreScore :: MonadServer m => Kind.COps -> m HighScore.ScoreTable+restoreScore Kind.COps{corule} = do+ let stdRuleset = Kind.stdRuleset corule+ scoresFile = rscoresFile stdRuleset+ dataDir <- liftIO appDataDir+ let path = dataDir </> scoresFile configExists <- liftIO $ doesFileExist path- if not configExists- then return HighScore.empty- else liftIO $ strictDecodeEOF path+ mscore <- liftIO $ do+ res <- Ex.try $+ if configExists then do+ s <- strictDecodeEOF path+ return $ Just s+ else return Nothing+ let handler :: Ex.SomeException -> IO (Maybe a)+ handler e = do+ let msg = "High score restore failed. The error message is:"+ <+> (T.unwords . T.lines) (tshow e)+ delayPrint $ msg+ return Nothing+ either handler return res+ maybe (return HighScore.empty) return mscore -- | Generate a new score, register it and save. registerScore :: MonadServer m => Status -> Maybe Actor -> FactionId -> m () registerScore status mbody fid = do+ cops@Kind.COps{corule} <- getsState scops assert (maybe True ((fid ==) . bfid) mbody) skip- factionD <- getsState sfactionD- let fact = factionD EM.! fid+ fact <- getsState $ (EM.! fid) . sfactionD assert (playerHuman $ gplayer fact) skip total <- case mbody of Just body -> getsState $ snd . calculateTotal body@@ -214,27 +228,70 @@ Just aid -> do b <- getsState $ getActorBody aid getsState $ snd . calculateTotal b- config@Config{configAppDataDir, configScoresFile} <- getsServer sconfig+ let stdRuleset = Kind.stdRuleset corule+ scoresFile = rscoresFile stdRuleset+ dataDir <- liftIO appDataDir -- Re-read the table in case it's changed by a concurrent game.- table <- restoreScore config+ table <- restoreScore cops time <- getsState stime date <- liftIO getClockTime- let path = configAppDataDir </> configScoresFile+ DebugModeSer{sdifficultySer} <- getsServer sdebugSer+ let path = dataDir </> scoresFile saveScore (ntable, _) = liftIO $ encodeEOF path (ntable :: HighScore.ScoreTable)- maybe skip saveScore $ HighScore.register table total time status date+ diff | not $ playerUI $ gplayer fact = 0+ | otherwise = sdifficultySer+ maybe skip saveScore $ HighScore.register table total time status date diff resetSessionStart :: MonadServer m => m () resetSessionStart = do sstart <- liftIO getClockTime modifyServer $ \ser -> ser {sstart} +-- TODO: all this breaks when games are loaded; we'd need to save+-- elapsed game clock time to fix this.+resetGameStart :: MonadServer m => m ()+resetGameStart = do+ sgstart <- liftIO getClockTime+ time <- getsState stime+ modifyServer $ \ser ->+ ser {sgstart, sallTime = timeAdd (sallTime ser) time}+ elapsedSessionTimeGT :: MonadServer m => Int -> m Bool elapsedSessionTimeGT stopAfter = do current <- liftIO getClockTime TOD s p <- getsServer sstart- return $ TOD (s + fromIntegral stopAfter) p <= current+ return $! TOD (s + fromIntegral stopAfter) p <= current +tellAllClipPS :: MonadServer m => m ()+tellAllClipPS = do+ bench <- getsServer $ sbenchmark . sdebugSer+ when bench $ do+ TOD s p <- getsServer sstart+ TOD sCur pCur <- liftIO getClockTime+ allTime <- getsServer sallTime+ gtime <- getsState stime+ let time = timeAdd allTime gtime+ let diff = fromIntegral sCur + fromIntegral pCur / 10e12+ - fromIntegral s - fromIntegral p / 10e12+ cps = fromIntegral (timeFit time timeClip) / diff :: Double+ debugPrint $ "Session time:" <+> tshow diff <> "s."+ <+> "Average clips per second:" <+> tshow cps <> "."++tellGameClipPS :: MonadServer m => m ()+tellGameClipPS = do+ bench <- getsServer $ sbenchmark . sdebugSer+ when bench $ do+ TOD s p <- getsServer sgstart+ unless (s == 0) $ do -- loaded game, don't report anything+ TOD sCur pCur <- liftIO getClockTime+ time <- getsState stime+ let diff = fromIntegral sCur + fromIntegral pCur / 10e12+ - fromIntegral s - fromIntegral p / 10e12+ cps = fromIntegral (timeFit time timeClip) / diff :: Double+ debugPrint $ "Game time:" <+> tshow diff <> "s."+ <+> "Average clips per second:" <+> tshow cps <> "."+ revealItems :: (MonadAtomic m, MonadServer m) => Maybe FactionId -> Maybe Actor -> m () revealItems mfid mbody = do@@ -288,22 +345,22 @@ factionD <- getsState sfactionD let assocsInGame = filter (inGame . snd) $ EM.assocs factionD keysInGame = map fst assocsInGame- assocsSpawn = filter (isSpawnFact cops . snd) assocsInGame- assocsNotSummon = filter (not . isSummonFact cops . snd) assocsInGame+ assocsNotHorror = filter (not . isHorrorFact cops . snd) assocsInGame assocsUI = filter (playerUI . gplayer . snd) assocsInGame- case assocsNotSummon of+ case assocsNotHorror of _ | null assocsUI ->- -- All non-UI players left in the game win.+ -- Only non-UI players left in the game and they all win. mapQuitF status{stOutcome=Conquer} keysInGame [] ->- -- Only summons remain so all win, UI or human or not, allied or not.+ -- Only horrors remain, so they win. mapQuitF status{stOutcome=Conquer} keysInGame- (_, fact1) : rest | null assocsSpawn && all (isAllied fact1 . fst) rest ->- -- Only one allied team remains in a no-spawners game.+ (_, fact1) : rest | all (not . isAtWar fact1 . fst) rest ->+ -- Nobody is at war any more, so all win.+ -- TODO: check not at war with each other. mapQuitF status{stOutcome=Conquer} keysInGame _ | stOutcome status == Escape -> do- -- Otherwise, in a spawners game or a game with many teams alive,- -- only complete Victory matters.+ -- Otherwise, in a game with many warring teams alive,+ -- only complete Victory matters, until enough of them die. let (victors, losers) = partition (flip isAllied fid . snd) assocsInGame mapQuitF status{stOutcome=Escape} $ map fst victors mapQuitF status{stOutcome=Defeated} $ map fst losers@@ -312,18 +369,14 @@ tryRestore :: MonadServer m => Kind.COps -> DebugModeSer -> m (Maybe (State, StateServer)) tryRestore Kind.COps{corule} sdebugSer = do- let pathsDataFile = rpathsDataFile $ Kind.stdRuleset corule+ let stdRuleset = Kind.stdRuleset corule+ scoresFile = rscoresFile stdRuleset+ pathsDataFile = rpathsDataFile stdRuleset prefix = ssavePrefixSer sdebugSer- -- A throw-away copy of rules config, to be used until the old- -- version of the config can be read from the savefile.- (Config{ configAppDataDir- , configRulesCfgFile- , configScoresFile }, _, _) <- mkConfigRules corule Nothing- let copies =- [ (configRulesCfgFile <.> ".default", configRulesCfgFile <.> ".ini")- , (configScoresFile, configScoresFile) ]+ let copies = [( "GameDefinition" </> scoresFile+ , scoresFile )] name = fromMaybe "save" prefix <.> saveName- liftIO $ Save.restoreGame name configAppDataDir copies pathsDataFile+ liftIO $ Save.restoreGame name copies pathsDataFile -- Global variable for all children threads of the server. childrenServer :: MVar [MVar ()]@@ -339,7 +392,7 @@ -> ChanServer CmdClientUI CmdSer -> IO ()) -> (FactionId- -> ChanServer CmdClientAI CmdSerTakeTime+ -> ChanServer CmdClientAI CmdTakeTimeSer -> IO ()) -> m () updateConn executorUI executorAI = do@@ -349,7 +402,7 @@ mkChanServer = do fromServer <- STM.newTQueueIO toServer <- STM.newTQueueIO- return ChanServer{..}+ return $! ChanServer{..} mkChanFrontend :: IO Frontend.ChanFrontend mkChanFrontend = STM.newTQueueIO addConn :: FactionId -> Faction -> IO ConnServerFaction@@ -417,62 +470,13 @@ g <- getsServer srandom let (a, ng) = St.runState r g modifyServer $ \ser -> ser {srandom = ng}- return a---- | Gets a random generator from the config or,--- if not present, generates one and updates the config with it.-getSetGen :: ConfigIO.CP -- ^ config- -> String -- ^ name of the generator- -> Maybe R.StdGen- -> IO (R.StdGen, ConfigIO.CP)-getSetGen config option mrandom =- case ConfigIO.getOption config "engine" option of- Just sg -> return (read sg, config)- Nothing -> do- -- Pick the randomly chosen generator from the IO monad (unless given)- -- and record it in the config for debugging (can be 'D'umped).- g <- case mrandom of- Just rnd -> return rnd- Nothing -> R.newStdGen- let gs = show g- c = ConfigIO.set config "engine" option gs- return (g, c)--parseConfigRules :: FilePath -> ConfigIO.CP -> Config-parseConfigRules dataDir cp =- let configSelfString = ConfigIO.to_string cp- configFirstDeathEnds = ConfigIO.get cp "engine" "firstDeathEnds"- configFovMode = ConfigIO.get cp "engine" "fovMode"- configSaveBkpClips = ConfigIO.get cp "engine" "saveBkpClips"- configAppDataDir = dataDir- configScoresFile = ConfigIO.get cp "file" "scoresFile"- configRulesCfgFile = "config.rules"- configSavePrefix = ConfigIO.get cp "file" "savePrefix"- configHeroNames =- let toNumber (ident, name) =- case stripPrefix "HeroName_" ident of- Just n -> (read n, T.pack name)- Nothing -> assert `failure` "wrong hero name id" `twith` ident- section = ConfigIO.getItems cp "heroName"- in map toNumber section- in Config{..}+ return $! a --- | Read and parse rules config file and supplement it with random seeds.--- This creates a server config file. Warning: when it's used, the game state--- may still be undefined, hence the content ops are given as an argument.-mkConfigRules :: MonadServer m- => Kind.Ops RuleKind -> Maybe R.StdGen- -> m (Config, R.StdGen, R.StdGen)-mkConfigRules corule mrandom = do- let cpRulesDefault = rcfgRulesDefault $ Kind.stdRuleset corule- dataDir <-- liftIO $ ConfigIO.appDataDir- cpRules <-- liftIO $ ConfigIO.mkConfig cpRulesDefault $ dataDir </> "config.rules.ini"- (dungeonGen, cp2) <-- liftIO $ getSetGen cpRules "dungeonRandomGenerator" mrandom- (startingGen, cp3) <-- liftIO $ getSetGen cp2 "startingRandomGenerator" mrandom- let conf = parseConfigRules dataDir cp3- -- Catch syntax errors ASAP.- return $! deepseq conf (conf, dungeonGen, startingGen)+-- | Gets a random generator from the arguments or, if not present,+-- generates one.+getSetGen :: MonadServer m+ => Maybe R.StdGen+ -> m R.StdGen+getSetGen mrng = case mrng of+ Just rnd -> return rnd+ Nothing -> liftIO $ R.newStdGen
Game/LambdaHack/Server/Action/ActionType.hs view
@@ -19,7 +19,6 @@ import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State import Game.LambdaHack.Server.Action.ActionClass-import Game.LambdaHack.Server.Config import Game.LambdaHack.Server.State data SerState = SerState@@ -39,18 +38,22 @@ getsState f = ActionSer $ gets $ f . serState instance MonadAction ActionSer where- modifyState f =- ActionSer $ modify $ \serS -> serS {serState = f $ serState serS}- putState s =- ActionSer $ modify $ \serS -> serS {serState = s}+ modifyState f = ActionSer $ state $ \serS ->+ let newSerS = serS {serState = f $ serState serS}+ in newSerS `seq` ((), newSerS)+ putState s = ActionSer $ state $ \serS ->+ let newSerS = serS {serState = s}+ in newSerS `seq` ((), newSerS) instance MonadServer ActionSer where getServer = ActionSer $ gets serServer getsServer f = ActionSer $ gets $ f . serServer- modifyServer f =- ActionSer $ modify $ \serS -> serS {serServer = f $ serServer serS}- putServer s =- ActionSer $ modify $ \serS -> serS {serServer = s}+ modifyServer f = ActionSer $ state $ \serS ->+ let newSerS = serS {serServer = f $ serServer serS}+ in newSerS `seq` ((), newSerS)+ putServer s = ActionSer $ state $ \serS ->+ let newSerS = serS {serServer = s}+ in newSerS `seq` ((), newSerS) liftIO = ActionSer . IO.liftIO saveServer = ActionSer $ do s <- gets serState@@ -70,14 +73,13 @@ executorSer :: ActionSer () -> IO () executorSer m = let saveFile (_, ser) =- configAppDataDir (sconfig ser)- </> fromMaybe "save" (ssavePrefixSer (sdebugSer ser))+ fromMaybe "save" (ssavePrefixSer (sdebugSer ser)) <.> saveName- exe toSave =+ exe serToSave = evalStateT (runActionSer m) SerState { serState = emptyState , serServer = emptyStateServer , serDict = EM.empty- , serToSave = toSave+ , serToSave } in Save.wrapInSaves saveFile exe
Game/LambdaHack/Server/AtomicSemSer.hs view
@@ -5,6 +5,7 @@ ( atomicSendSem ) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES@@ -25,7 +26,6 @@ import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Server.Action import Game.LambdaHack.Server.State-import Control.Exception.Assert.Sugar storeUndo :: MonadServer m => Atomic -> m () storeUndo _atomic =@@ -101,14 +101,12 @@ resetFidPerception fid lid perNew <- getPerFid fid lid let inPer = diffPer perNew perOld- inPA = perActor inPer outPer = diffPer perOld perNew- outPA = perActor outPer- if EM.null outPA && EM.null inPA+ if nullPer outPer && nullPer inPer then anySend fid perOld perOld else do- sendA fid $ PerceptionA lid outPA inPA unless knowEvents $ do -- inconsistencies would quickly manifest+ sendA fid $ PerceptionA lid outPer inPer let remember = atomicRemember lid inPer sOld seenNew = seenAtomicCli False fid perNew seenOld = seenAtomicCli False fid perOld@@ -147,8 +145,8 @@ let inFov = ES.elems $ totalVisible inPer lvl = sdungeon s EM.! lid -- Actors.- inPrio = mapMaybe (\p -> posToActor p lid s) inFov- fActor aid = SpotActorA aid (getActorBody aid s) (getActorItem aid s)+ inPrio = concatMap (\p -> posToActors p lid s) inFov+ fActor ((aid, b), ais) = SpotActorA aid b ais inActor = map fActor inPrio -- Items. pMaybe p = maybe Nothing (\x -> Just (p, x))@@ -171,7 +169,6 @@ -- TODO: make floor paths from hidden tiles -- TODO: perhaps decrease secrecy as lseen, ltime or lsmell increases -- or a per-party counter increases- -- TODO: give spawning factions a bonus to searching -- Smells. inSmellFov = ES.elems $ smellVisible inPer inSm = mapMaybe (\p -> pMaybe p $ EM.lookup p (lsmell lvl)) inSmellFov
− Game/LambdaHack/Server/Config.hs
@@ -1,53 +0,0 @@--- | Personal game configuration file type definitions.-module Game.LambdaHack.Server.Config- ( Config(..)- ) where--import Control.DeepSeq-import Data.Binary-import Data.Text (Text)--import Game.LambdaHack.Server.Fov---- | Fully typed contents of the rules config file. This config--- is a part of the game server.-data Config = Config- { configSelfString :: !String- -- engine- , configFirstDeathEnds :: !Bool- , configFovMode :: !FovMode- , configSaveBkpClips :: !Int- -- files- , configAppDataDir :: !FilePath- , configScoresFile :: !FilePath- , configRulesCfgFile :: !FilePath- , configSavePrefix :: !String- -- heroNames- , configHeroNames :: ![(Int, Text)]- }- deriving Show--instance NFData Config--instance Binary Config where- put Config{..} = do- put configSelfString- put configFirstDeathEnds- put configFovMode- put configSaveBkpClips- put configAppDataDir- put configScoresFile- put configRulesCfgFile- put configSavePrefix- put configHeroNames- get = do- configSelfString <- get- configFirstDeathEnds <- get- configFovMode <- get- configSaveBkpClips <- get- configAppDataDir <- get- configScoresFile <- get- configRulesCfgFile <- get- configSavePrefix <- get- configHeroNames <- get- return Config{..}
Game/LambdaHack/Server/DungeonGen.hs view
@@ -3,7 +3,7 @@ ( FreshDungeon(..), dungeonGen ) where -import Control.Arrow (first)+import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM import Data.List@@ -15,7 +15,7 @@ import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY+import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Random import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time@@ -23,24 +23,23 @@ import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Server.DungeonGen.Area-import Game.LambdaHack.Server.DungeonGen.Cave hiding (TileMapXY)+import Game.LambdaHack.Server.DungeonGen.Cave import Game.LambdaHack.Server.DungeonGen.Place-import Control.Exception.Assert.Sugar import Game.LambdaHack.Utils.Frequency --convertTileMaps :: Rnd (Kind.Id TileKind) -> Int -> Int -> TileMapXY+convertTileMaps :: Rnd (Kind.Id TileKind) -> Int -> Int -> TileMapEM -> Rnd TileMap convertTileMaps cdefTile cxsize cysize ltile = do- let bounds = (origin, toPoint cxsize $ PointXY (cxsize - 1, cysize - 1))- assocs = map (first (toPoint cxsize)) (EM.assocs ltile)- pickedTiles <- replicateM (cxsize * cysize) cdefTile- return $ Kind.listArray bounds pickedTiles Kind.// assocs+ let f :: Point -> Rnd (Kind.Id TileKind)+ f p = case EM.lookup p ltile of+ Just t -> return t+ Nothing -> cdefTile+ PointArray.generateMA cxsize cysize f placeStairs :: Kind.Ops TileKind -> TileMap -> CaveKind -> [Point] -> Rnd Point placeStairs cotile cmap CaveKind{..} ps = do- let dist cmin l _ = all (\pos -> chessDist cxsize l pos > cmin) ps+ let dist cmin l _ = all (\pos -> chessDist l pos > cmin) ps findPosTry 1000 cmap (\p t -> Tile.hasFeature cotile F.CanActor t && dist 0 p t) -- can't overwrite stairs with other stairs@@ -57,15 +56,15 @@ , cocave=Kind.Ops{okind=cokind} } Cave{..} ldepth minD maxD nstairUp escapeFeature = do let kc@CaveKind{..} = cokind dkind- fitArea pos = inside cxsize pos . fromArea . qarea- findLegend pos = maybe clitLegendTile qlegend+ fitArea pos = inside pos . fromArea . qarea+ findLegend pos = maybe clegendLitTile qlegend $ find (fitArea pos) dplaces- hasEscapeAndSymbol sym t = Tile.kindHasFeature (F.Cause Effect.Escape) t- && tsymbol t == sym+ hasEscape p t = Tile.kindHasFeature (F.Cause $ Effect.Escape p) t ascendable = Tile.kindHasFeature $ F.Cause (Effect.Ascend 1) descendable = Tile.kindHasFeature $ F.Cause (Effect.Ascend (-1)) dcond kt = not (Tile.kindHasFeature F.Clear kt)- || (if dnight then not else id) (Tile.kindHasFeature F.Lit kt)+ || (if dnight then id else not)+ (Tile.kindHasFeature F.Dark kt) pickDefTile = fmap (fromMaybe $ assert `failure` cdefTile) $ opick cdefTile dcond cmap <- convertTileMaps pickDefTile cxsize cysize dmap@@ -93,7 +92,7 @@ let st = (spos, stairId) asc = ascendable $ okind stairId desc = descendable $ okind stairId- return $ case (asc, desc) of+ return $! case (asc, desc) of (True, False) -> (st : up, down, upDown) (False, True) -> (up, st : down, upDown) (True, True) -> (up, down, st : upDown)@@ -122,15 +121,15 @@ Just True -> do let legend = findLegend epos upEscape <- fmap (fromMaybe $ assert `failure` legend)- $ opick legend $ hasEscapeAndSymbol '<'+ $ opick legend $ hasEscape 1 return [(epos, upEscape)] Just False -> do let legend = findLegend epos downEscape <- fmap (fromMaybe $ assert `failure` legend)- $ opick legend $ hasEscapeAndSymbol '>'+ $ opick legend $ hasEscape (-1) return [(epos, downEscape)] let exits = stairsTotal ++ escape- ltile = cmap Kind.// exits+ ltile = cmap PointArray.// exits -- We reverse the order in down stairs, to minimize long stair chains. lstair = ( map fst $ stairsUp ++ stairsUpDown , map fst $ stairsUpDown ++ stairsDown )@@ -140,14 +139,15 @@ assert (not $ nullFreq itemFreq) skip lsecret <- random return $! levelFromCaveKind cops kc ldepth ltile lstair- litemNum itemFreq lsecret+ litemNum itemFreq lsecret (isJust escapeFeature) levelFromCaveKind :: Kind.COps -> CaveKind -> Int -> TileMap -> ([Point], [Point])- -> Int -> Frequency Text -> Int+ -> Int -> Frequency Text -> Int -> Bool -> Level levelFromCaveKind Kind.COps{cotile}- CaveKind{..} ldepth ltile lstair litemNum litemFreq lsecret =+ CaveKind{..}+ ldepth ltile lstair litemNum litemFreq lsecret lescape = Level { ldepth , lprio = EM.empty@@ -159,14 +159,15 @@ , ldesc = cname , lstair , lseen = 0- , lclear = let f !n !tk | Tile.isExplorable cotile tk = n + 1- | otherwise = n- in Kind.foldlArray f 0 ltile+ , lclear = let f n t | Tile.isExplorable cotile t = n + 1+ | otherwise = n+ in PointArray.foldlA f 0 ltile , ltime = timeTurn , litemNum , litemFreq , lsecret , lhidden = chidden+ , lescape } findGenerator :: Kind.COps -> Caves@@ -210,4 +211,4 @@ assert (nstairUpLast == 0) skip let freshDungeon = EM.fromList levels freshDepth = totalDepth- return FreshDungeon{..}+ return $! FreshDungeon{..}
Game/LambdaHack/Server/DungeonGen/Area.hs view
@@ -5,7 +5,7 @@ import Data.Binary -import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Point -- | The type of areas. The bottom left and the top right points. data Area = Area !X !Y !X !Y@@ -20,19 +20,19 @@ fromArea :: Area -> (X, Y, X, Y) fromArea (Area x0 y0 x1 y1) = (x0, y0, x1, y1) -trivialArea :: PointXY -> Area-trivialArea (PointXY (x, y)) = Area x y x y+trivialArea :: Point -> Area+trivialArea (Point x y) = Area x y x y -- | Divide uniformly a larger area into the given number of smaller areas -- overlapping at the edges.-grid :: (X, Y) -> Area -> [(PointXY, Area)]+grid :: (X, Y) -> Area -> [(Point, Area)] grid (nx, ny) (Area x0 y0 x1 y1) = let xd = x1 - x0 -- not +1, because we need overlap yd = y1 - y0- in [ (PointXY (x, y), Area (x0 + xd * x `div` nx)- (y0 + yd * y `div` ny)- (x0 + xd * (x + 1) `div` nx)- (y0 + yd * (y + 1) `div` ny))+ in [ (Point x y, Area (x0 + xd * x `div` nx)+ (y0 + yd * y `div` ny)+ (x0 + xd * (x + 1) `div` nx)+ (y0 + yd * (y + 1) `div` ny)) | x <- [0..nx-1], y <- [0..ny-1] ] -- | Enlarge (or shrink) the given area on all fours sides by the amount.
Game/LambdaHack/Server/DungeonGen/AreaRnd.hs view
@@ -8,24 +8,24 @@ , Corridor, connectPlaces ) where -import qualified Data.EnumSet as ES+import Control.Exception.Assert.Sugar import Data.Maybe+import qualified Data.Set as S -import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Random-import Game.LambdaHack.Common.VectorXY+import Game.LambdaHack.Common.Vector import Game.LambdaHack.Server.DungeonGen.Area-import Control.Exception.Assert.Sugar -- Picking random points inside areas -- | Pick a random point within an area.-xyInArea :: Area -> Rnd PointXY+xyInArea :: Area -> Rnd Point xyInArea area = do let (x0, y0, x1, y1) = fromArea area rx <- randomR (x0, x1) ry <- randomR (y0, y1)- return $ PointXY (rx, ry)+ return $! Point rx ry -- | Create a random room according to given parameters. mkRoom :: (X, Y) -- ^ minimum size@@ -37,17 +37,17 @@ assert (xm <= x1 - x0 + 1 && ym <= y1 - y0 + 1) skip let a0 = (x0, y0, x1 - xm + 1, y1 - ym + 1) area0 = fromMaybe (assert `failure` a0) $ toArea a0- PointXY (rx0, ry0) <- xyInArea area0+ Point rx0 ry0 <- xyInArea area0 let sX = rx0 + xm - 1 sY = ry0 + ym - 1 eX = min x1 (rx0 + xM - 1) eY = min y1 (ry0 + yM - 1) a1 = (sX, sY, eX, eY) area1 = fromMaybe (assert `failure` a1) $ toArea a1- PointXY (rx1, ry1) <- xyInArea area1+ Point rx1 ry1 <- xyInArea area1 let a3 = (rx0, ry0, rx1, ry1) area3 = fromMaybe (assert `failure` a3) $ toArea a3- return area3+ return $! area3 -- | Create a void room, i.e., a single point area within the designated area. mkVoidRoom :: Area -> Rnd Area@@ -55,45 +55,50 @@ -- Pass corridors closer to the middle of the grid area, if possible. let core = fromMaybe area $ shrink area pxy <- xyInArea core- return $ trivialArea pxy+ return $! trivialArea pxy -- Choosing connections between areas in a grid -- | Pick a subset of connections between adjacent areas within a grid until -- there is only one connected component in the graph of all areas.-connectGrid :: (X, Y) -> Rnd [(PointXY, PointXY)]+connectGrid :: (X, Y) -> Rnd [(Point, Point)] connectGrid (nx, ny) = do- let unconnected = ES.fromList [ PointXY (x, y)+ let unconnected = S.fromList [ Point x y | x <- [0..nx-1], y <- [0..ny-1] ] -- Candidates are neighbours that are still unconnected. We start with -- a random choice. rx <- randomR (0, nx-1) ry <- randomR (0, ny-1)- let candidates = ES.fromList [ PointXY (rx, ry) ]+ let candidates = S.fromList [Point rx ry] connectGrid' (nx, ny) unconnected candidates [] -connectGrid' :: (X, Y) -> ES.EnumSet PointXY -> ES.EnumSet PointXY- -> [(PointXY, PointXY)]- -> Rnd [(PointXY, PointXY)]+connectGrid' :: (X, Y) -> S.Set Point -> S.Set Point+ -> [(Point, Point)]+ -> Rnd [(Point, Point)] connectGrid' (nx, ny) unconnected candidates acc- | ES.null candidates = return $ map sortPointXY acc+ | S.null candidates = return $! map sortPoint acc | otherwise = do- c <- oneOf (ES.toList candidates)+ c <- oneOf (S.toList candidates) -- potential new candidates:- let ns = ES.fromList $ vicinityCardinalXY (0, 0, nx-1, ny-1) c- nu = ES.delete c unconnected -- new unconnected+ let ns = S.fromList $ vicinityCardinal nx ny c+ nu = S.delete c unconnected -- new unconnected -- (new candidates, potential connections):- (nc, ds) = ES.partition (`ES.member` nu) ns- new <- if ES.null ds+ (nc, ds) = S.partition (`S.member` nu) ns+ new <- if S.null ds then return id else do- d <- oneOf (ES.toList ds)+ d <- oneOf (S.toList ds) return ((c, d) :) connectGrid' (nx, ny) nu- (ES.delete c (candidates `ES.union` nc)) (new acc)+ (S.delete c (candidates `S.union` nc)) (new acc) +-- | Sort the sequence of two points, in the derived lexicographic order.+sortPoint :: (Point, Point) -> (Point, Point)+sortPoint (a, b) | a <= b = (a, b)+ | otherwise = (b, a)+ -- | Pick a single random connection between adjacent areas within a grid.-randomConnection :: (X, Y) -> Rnd (PointXY, PointXY)+randomConnection :: (X, Y) -> Rnd (Point, Point) randomConnection (nx, ny) = assert (nx > 1 && ny > 0 || nx > 0 && ny > 1 `blame` "wrong connection" `twith` (nx, ny)) $ do@@ -102,11 +107,11 @@ then do rx <- randomR (0, nx-2) ry <- randomR (0, ny-1)- return (PointXY (rx, ry), PointXY (rx+1, ry))+ return (Point rx ry, Point (rx+1) ry) else do rx <- randomR (0, nx-1) ry <- randomR (0, ny-2)- return (PointXY (rx, ry), PointXY (rx, ry+1))+ return (Point rx ry, Point rx (ry+1)) -- Plotting individual corridors between two areas @@ -114,20 +119,20 @@ data HV = Horiz | Vert -- | The coordinates of consecutive fields of a corridor.-type Corridor = [PointXY]+type Corridor = [Point] -- | Create a corridor, either horizontal or vertical, with -- a possible intermediate part that is in the opposite direction. mkCorridor :: HV -- ^ orientation of the starting section- -> PointXY -- ^ starting point- -> PointXY -- ^ ending point+ -> Point -- ^ starting point+ -> Point -- ^ ending point -> Area -- ^ the area containing the intermediate point -> Rnd Corridor -- ^ straight sections of the corridor-mkCorridor hv (PointXY (x0, y0)) (PointXY (x1, y1)) b = do- PointXY (rx, ry) <- xyInArea b- case hv of- Horiz -> return $ map PointXY [(x0, y0), (rx, y0), (rx, y1), (x1, y1)]- Vert -> return $ map PointXY [(x0, y0), (x0, ry), (x1, ry), (x1, y1)]+mkCorridor hv (Point x0 y0) (Point x1 y1) b = do+ Point rx ry <- xyInArea b+ return $! map (uncurry Point) $ case hv of+ Horiz -> [(x0, y0), (rx, y0), (rx, y1), (x1, y1)]+ Vert -> [(x0, y0), (x0, ry), (x1, ry), (x1, y1)] -- | Try to connect two interiors of places with a corridor. -- Choose entrances at least 4 or 3 tiles distant from the edges, if the place@@ -150,8 +155,8 @@ (nx0, nx1) = trim4 (x0, x1) (ny0, ny1) = trim4 (y0, y1) in fromMaybe (assert `failure` area) $ toArea (nx0, ny0, nx1, ny1)- PointXY (sx, sy) <- xyInArea $ trim so- PointXY (tx, ty) <- xyInArea $ trim to+ Point sx sy <- xyInArea $ trim so+ Point tx ty <- xyInArea $ trim to let hva sarea tarea = do let (_, _, zsx1, zsy1) = fromArea sarea (ztx0, zty0, _, _) = fromArea tarea@@ -176,8 +181,8 @@ -- We cross width one places completely with the corridor, for void -- rooms and others (e.g., one-tile wall room then becomes a door, etc.). let (p0, p1) = case hv of- Horiz -> (PointXY (sox1, sy), PointXY (tox0, ty))- Vert -> (PointXY (sx, soy1), PointXY (tx, toy0))+ Horiz -> (Point sox1 sy, Point tox0 ty)+ Vert -> (Point sx soy1, Point tx toy0) -- The condition imposed on mkCorridor are tricky: there might not always -- exist a good intermediate point if the places are allowed to be close -- together and then we let the intermediate part degenerate.
Game/LambdaHack/Server/DungeonGen/Cave.hs view
@@ -1,18 +1,19 @@ -- | Generation of caves (not yet inhabited dungeon levels) from cave kinds. module Game.LambdaHack.Server.DungeonGen.Cave- ( TileMapXY, ItemFloorXY, Cave(..), buildCave+ ( Cave(..), buildCave ) where import Control.Arrow ((&&&))+import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM-import qualified Data.List as L+import Data.List+import qualified Data.Map.Strict as M import Data.Maybe+import qualified Data.Traversable as Traversable -import qualified Game.LambdaHack.Common.Feature as F-import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Kind as Kind-import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Random import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Content.CaveKind@@ -20,24 +21,12 @@ import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Server.DungeonGen.Area import Game.LambdaHack.Server.DungeonGen.AreaRnd-import Game.LambdaHack.Server.DungeonGen.Place hiding (TileMapXY)-import qualified Game.LambdaHack.Server.DungeonGen.Place as Place-import Control.Exception.Assert.Sugar---- | The map of tile kinds in a cave.--- The map is sparse. The default tile that eventually fills the empty spaces--- is specified in the cave kind specification with @cdefTile@.-type TileMapXY = Place.TileMapXY---- | The map of starting items in tiles of a cave. The map is sparse.--- Unspecified tiles have no starting items.-type ItemFloorXY = EM.EnumMap PointXY (Item, Int)+import Game.LambdaHack.Server.DungeonGen.Place -- | The type of caves (not yet inhabited dungeon levels). data Cave = Cave { dkind :: !(Kind.Id CaveKind) -- ^ the kind of the cave- , dmap :: !TileMapXY -- ^ tile kinds in the cave- , ditem :: !ItemFloorXY -- ^ starting items in the cave+ , dmap :: !TileMapEM -- ^ tile kinds in the cave , dplaces :: ![Place] -- ^ places generated in the cave , dnight :: !Bool -- ^ whether the cave is dark }@@ -72,20 +61,21 @@ -> Int -- ^ maximum depth of the dungeon -> Kind.Id CaveKind -- ^ cave kind to use for generation -> Rnd Cave-buildCave cops@Kind.COps{ cotile=cotile@Kind.Ops{ opick- , ouniqGroup }+buildCave cops@Kind.COps{ cotile=cotile@Kind.Ops{opick} , cocave=Kind.Ops{okind} , coplace=Kind.Ops{okind=pokind} }- ln depth ci = do- let kc@CaveKind{..} = okind ci+ ln depth dkind = do+ let kc@CaveKind{..} = okind dkind lgrid@(gx, gy) <- castDiceXY cgrid -- Make sure that in caves not filled with rock, there is a passage -- across the cave, even if a single room blocks most of the cave.+ -- Also, ensure fancy outer fences are not obstructed by room walls. let fullArea = fromMaybe (assert `failure` kc) $ toArea (0, 0, cxsize - 1, cysize - 1) subFullArea = fromMaybe (assert `failure` kc) $ toArea (1, 1, cxsize - 2, cysize - 2)- area | gx == 1 || gy == 1 = subFullArea+ area | gx * gy == 1+ || couterFenceTile /= "basic outer fence" = subFullArea | otherwise = fullArea gs = grid lgrid area (addedConnects, voidPlaces) <- do@@ -110,8 +100,7 @@ else fmap Right $ mkRoom minPlaceSize maxPlaceSize innerArea return (i, r')) gs- let hardRockId = ouniqGroup "outer fence"- fence = buildFence hardRockId subFullArea+ fence <- buildFenceRnd cops couterFenceTile subFullArea dnight <- chanceDeep ln depth cnightChance darkCorTile <- fmap (fromMaybe $ assert `failure` cdarkCorTile) $ opick cdarkCorTile (const True)@@ -120,12 +109,13 @@ let pickedCorTile = if dnight then darkCorTile else litCorTile addPl (m, pls, qls) (i, Left r) = return (m, pls, (i, Left r) : qls) addPl (m, pls, qls) (i, Right r) = do- (tmap, place) <- buildPlace cops kc darkCorTile litCorTile ln depth r+ (tmap, place) <-+ buildPlace cops kc dnight darkCorTile litCorTile ln depth r return (EM.union tmap m, place : pls, (i, Right (r, place)) : qls) (lplaces, dplaces, qplaces0) <- foldM addPl (fence, [], []) places0 connects <- connectGrid lgrid- let allConnects = L.union connects addedConnects -- no duplicates- qplaces = EM.fromList qplaces0+ let allConnects = union connects addedConnects -- no duplicates+ qplaces = M.fromList qplaces0 cs <- mapM (\(p0, p1) -> do let shrinkPlace (r, Place{qkind}) = case shrink r of@@ -139,45 +129,47 @@ Just mergeArea -> (mergeArea, r) _ -> (sr, sr) shrinkForFence = either (id &&& id) shrinkPlace- rr0 = shrinkForFence $ qplaces EM.! p0- rr1 = shrinkForFence $ qplaces EM.! p1+ rr0 = shrinkForFence $ qplaces M.! p0+ rr1 = shrinkForFence $ qplaces M.! p1 connectPlaces rr0 rr1) allConnects- let lcorridors = EM.unions (L.map (digCorridors pickedCorTile) cs)+ let lcorridors = EM.unions (map (digCorridors pickedCorTile) cs) lm = EM.unionWith (mergeCorridor cotile) lcorridors lplaces -- Convert wall openings into doors, possibly.- let f l (p, t) =- if not $ Tile.hasFeature cotile F.Suspect t- then return l -- no opening to start with+ let f t =+ if not $ Tile.isSuspect cotile t+ -- May also turn a cache into a floor (and/or a pillar?); tough luck.+ -- TODO: the floor may be lit while it should be dark.+ then return t -- no opening to start with else do -- Openings have a certain chance to be doors -- and doors have a certain chance to be open. rd <- chance cdoorChance if not rd then- return $ EM.insert p pickedCorTile l -- opening kept+ let cor = if Tile.isLit cotile t then litCorTile else darkCorTile+ in return $! cor -- opening kept else do ro <- chance copenChance doorClosedId <- Tile.revealAs cotile t if not ro then- return $ EM.insert p doorClosedId l+ return $! doorClosedId else do doorOpenId <- Tile.openTo cotile doorClosedId- return $ EM.insert p doorOpenId l- dmap <- foldM f lm (EM.assocs lm)+ return $! doorOpenId+ dmap <- Traversable.mapM f lm let cave = Cave- { dkind = ci- , ditem = EM.empty+ { dkind , dmap , dplaces , dnight }- return cave+ return $! cave -digCorridors :: Kind.Id TileKind -> Corridor -> TileMapXY+digCorridors :: Kind.Id TileKind -> Corridor -> TileMapEM digCorridors tile (p1:p2:ps) = EM.union corPos (digCorridors tile (p2:ps)) where- corXY = fromTo p1 p2- corPos = EM.fromList $ L.zip corXY (repeat tile)+ cor = fromTo p1 p2+ corPos = EM.fromList $ zip cor (repeat tile) digCorridors _ _ = EM.empty mergeCorridor :: Kind.Ops TileKind -> Kind.Id TileKind -> Kind.Id TileKind
Game/LambdaHack/Server/DungeonGen/Place.hs view
@@ -1,26 +1,25 @@ {-# LANGUAGE RankNTypes #-} -- | Generation of places from place kinds. module Game.LambdaHack.Server.DungeonGen.Place- ( TileMapXY, Place(..), placeCheck, buildFence, buildPlace+ ( TileMapEM, Place(..), placeCheck, buildFenceRnd, buildPlace ) where +import Control.Exception.Assert.Sugar import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import qualified Data.List as L import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Misc-import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Random import Game.LambdaHack.Content.CaveKind import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Server.DungeonGen.Area-import Control.Exception.Assert.Sugar -- TODO: use more, rewrite as needed, document each field. -- | The parameters of a place. Most are immutable and set@@ -38,7 +37,7 @@ -- | The map of tile kinds in a place (and generally anywhere in a cave). -- The map is sparse. The default tile that eventually fills the empty spaces -- is specified in the cave kind specification with @cdefTile@.-type TileMapXY = EM.EnumMap PointXY (Kind.Id TileKind)+type TileMapEM = EM.EnumMap Point (Kind.Id TileKind) -- | For @CAlternate@ tiling, require the place be comprised -- of an even number of whole corners, with exactly one square@@ -56,7 +55,7 @@ dx = x1 - x0 + 1 dy = y1 - y0 + 1 dxcorner = case ptopLeft of [] -> 0 ; l : _ -> T.length l- dycorner = L.length ptopLeft+ dycorner = length ptopLeft wholeOverlapped d dcorner = d > 1 && dcorner > 1 && (d - 1) `mod` (2 * (dcorner - 1)) == 0 in case pcover of@@ -80,15 +79,16 @@ -- and fill a cave section acccording to it. buildPlace :: Kind.COps -- ^ the game content -> CaveKind -- ^ current cave kind+ -> Bool -- ^ whether the cave is dark -> Kind.Id TileKind -- ^ dark fence tile, if fence hollow -> Kind.Id TileKind -- ^ lit fence tile, if fence hollow -> Int -- ^ current level depth -> Int -- ^ maximum depth -> Area -- ^ whole area of the place, fence included- -> Rnd (TileMapXY, Place)+ -> Rnd (TileMapEM, Place) buildPlace Kind.COps{ cotile=cotile@Kind.Ops{opick=opick} , coplace=Kind.Ops{okind=pokind, opick=popick} }- CaveKind{..} darkCorTile litCorTile ln depth r = do+ CaveKind{..} dnight darkCorTile litCorTile ln depth r = do qsolidFence <- fmap (fromMaybe $ assert `failure` cfillerTile) $ opick cfillerTile (const True) dark <- chanceDeep ln depth cdarkChance@@ -97,56 +97,75 @@ $ popick cave (placeCheck r) let qhollowFence = if dark then darkCorTile else litCorTile kr = pokind qkind- qlegend = if dark then cdarkLegendTile else clitLegendTile+ qlegend = if dark then clegendDarkTile else clegendLitTile qseen = False qarea = fromMaybe (assert `failure` (kr, r)) $ interiorArea (pfence kr) r place = Place {..} legend <- olegend cotile qlegend+ legendLit <- olegend cotile clegendLitTile let xlegend = EM.insert 'X' qhollowFence legend- return (digPlace place kr xlegend, place)+ xlegendLit = EM.insert 'X' qhollowFence legendLit+ cmap = tilePlace qarea kr+ fence = case pfence kr of+ FWall -> buildFence qsolidFence qarea+ FFloor -> buildFence qhollowFence qarea+ FNone -> EM.empty+ (x0, y0, x1, y1) = fromArea qarea+ isEdge (Point x y) = x `elem` [x0, x1] || y `elem` [y0, y1]+ digNight xy c | isEdge xy = xlegendLit EM.! c+ | otherwise = xlegend EM.! c+ interior = case pfence kr of+ FNone | not dnight -> EM.mapWithKey digNight cmap+ _ -> EM.map (xlegend EM.!) cmap+ tmap = EM.union interior fence+ return (tmap, place) -- | Roll a legend of a place plan: a map from plan symbols to tile kinds. olegend :: Kind.Ops TileKind -> Text -> Rnd (EM.EnumMap Char (Kind.Id TileKind))-olegend Kind.Ops{ofoldrWithKey, opick} group =+olegend Kind.Ops{ofoldrWithKey, opick} cgroup = let getSymbols _ tk acc = maybe acc (const $ ES.insert (tsymbol tk) acc)- (L.lookup group $ tfreq tk)+ (lookup cgroup $ tfreq tk) symbols = ofoldrWithKey getSymbols ES.empty getLegend s acc = do m <- acc- tk <- fmap (fromMaybe $ assert `failure` (group, s))- $ opick group $ (== s) . tsymbol- return $ EM.insert s tk m+ tk <- fmap (fromMaybe $ assert `failure` (cgroup, s))+ $ opick cgroup $ (== s) . tsymbol+ return $! EM.insert s tk m legend = ES.foldr getLegend (return EM.empty) symbols in legend -- | Construct a fence around an area, with the given tile kind.-buildFence :: Kind.Id TileKind -> Area -> TileMapXY+buildFence :: Kind.Id TileKind -> Area -> TileMapEM buildFence fenceId area = let (x0, y0, x1, y1) = fromArea area- in EM.fromList $ [ (PointXY (x, y), fenceId)+ in EM.fromList $ [ (Point x y, fenceId) | x <- [x0-1, x1+1], y <- [y0..y1] ] ++- [ (PointXY (x, y), fenceId)+ [ (Point x y, fenceId) | x <- [x0-1..x1+1], y <- [y0-1, y1+1] ] --- | Construct a place of the given kind, with the given fence tile.-digPlace :: Place -- ^ the place parameters- -> PlaceKind -- ^ the place kind- -> EM.EnumMap Char (Kind.Id TileKind) -- ^ the legend- -> TileMapXY-digPlace Place{..} kr legend =- let fence = case pfence kr of- FWall -> buildFence qsolidFence qarea- FFloor -> buildFence qhollowFence qarea- FNone -> EM.empty- in EM.union (EM.map (legend EM.!) $ tilePlace qarea kr) fence+-- | Construct a fence around an area, with the given tile group.+buildFenceRnd :: Kind.COps -> Text -> Area -> Rnd TileMapEM+buildFenceRnd Kind.COps{cotile=Kind.Ops{opick}} couterFenceTile area = do+ let (x0, y0, x1, y1) = fromArea area+ fenceIdRnd (xf, yf) = do+ let isCorner x y = x `elem` [x0-1, x1+1] && y `elem` [y0-1, y1+1]+ tileGroup | isCorner xf yf = "basic outer fence"+ | otherwise = couterFenceTile+ fenceId <- fmap (fromMaybe $ assert `failure` tileGroup)+ $ opick tileGroup (const True)+ return (Point xf yf, fenceId)+ pointList = [ (x, y) | x <- [x0-1, x1+1], y <- [y0..y1] ]+ ++ [ (x, y) | x <- [x0-1..x1+1], y <- [y0-1, y1+1] ]+ fenceList <- mapM fenceIdRnd pointList+ return $! EM.fromList fenceList -- 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- -> EM.EnumMap PointXY Char+ -> EM.EnumMap Point Char tilePlace area pl@PlaceKind{..} = let (x0, y0, x1, y1) = fromArea area xwidth = x1 - x0 + 1@@ -155,33 +174,34 @@ [] -> assert `failure` (area, pl) l : _ -> T.length l (dx, dy) = assert (xwidth >= dxcorner && ywidth >= length ptopLeft- `blame` (area, pl)) (xwidth, ywidth)- fromX (x, y) = L.map PointXY $ L.zip [x..] (repeat y)- fillInterior :: (forall a. Int -> [a] -> [a]) -> [(PointXY, Char)]+ `blame` (area, pl))+ (xwidth, ywidth)+ fromX (x2, y2) =+ zipWith (\x y -> Point x y) [x2..] (repeat y2)+ fillInterior :: (forall a. Int -> [a] -> [a]) -> [(Point, Char)] fillInterior f =- let tileInterior (y, row) = L.zip (fromX (x0, y)) $ f dx row- reflected = L.zip [y0..] $ f dy $ map T.unpack ptopLeft- in L.concatMap tileInterior reflected+ let tileInterior (y, row) = zip (fromX (x0, y)) $ f dx row+ reflected = zip [y0..] $ f dy $ map T.unpack ptopLeft+ in concatMap tileInterior reflected tileReflect :: Int -> [a] -> [a] tileReflect d pat =- let lstart = L.take (d `divUp` 2) pat- lend = L.take (d `div` 2) pat- in lstart ++ L.reverse lend+ let lstart = take (d `divUp` 2) pat+ lend = take (d `div` 2) pat+ in lstart ++ reverse lend interior = case pcover of CAlternate -> let tile :: Int -> [a] -> [a] tile _ [] = assert `failure` "nothing to tile" `twith` pl- tile d pat =- L.take d (L.cycle $ L.init pat ++ L.init (L.reverse pat))+ tile d pat = take d (cycle $ init pat ++ init (reverse pat)) in fillInterior tile CStretch -> let stretch :: Int -> [a] -> [a] stretch _ [] = assert `failure` "nothing to stretch" `twith` pl- stretch d pat = tileReflect d (pat ++ L.repeat (L.last pat))+ stretch d pat = tileReflect d (pat ++ repeat (last pat)) in fillInterior stretch CReflect -> let reflect :: Int -> [a] -> [a]- reflect d pat = tileReflect d (L.cycle pat)+ reflect d pat = tileReflect d (cycle pat) in fillInterior reflect in EM.fromList interior @@ -200,4 +220,4 @@ qlegend <- get qsolidFence <- get qhollowFence <- get- return Place{..}+ return $! Place{..}
Game/LambdaHack/Server/EffectSem.hs view
@@ -4,9 +4,11 @@ ( -- + Semantics of effects itemEffect, effectSem -- * Assorted operations- , createItems, addHero, spawnMonsters, pickFaction, electLeader, deduceKilled+ , registerItem, createItems, addHero, spawnMonsters+ , electLeader, deduceKilled ) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM@@ -18,7 +20,6 @@ import Data.Text (Text) import qualified NLP.Miniutter.English as MU -import Control.Exception.Assert.Sugar import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState@@ -37,8 +38,8 @@ import Game.LambdaHack.Content.ActorKind import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ModeKind+import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Server.Action-import Game.LambdaHack.Server.Config import Game.LambdaHack.Server.State import Game.LambdaHack.Utils.Frequency @@ -53,14 +54,16 @@ => ActorId -> ActorId -> Maybe ItemId -> Item -> m () itemEffect source target miid item = do- sb <- getsState $ getActorBody source discoS <- getsServer sdisco let ik = fromJust $ jkind discoS item ef = jeffect item b <- effectSem ef source target -- The effect is interesting so the item gets identified, if seen- -- (the item is in source actor's inventory, so his position is given).- let atomic iid = execCmdAtomic $ DiscoverA (blid sb) (bpos sb) iid ik+ -- (the item is in source actor's inventory, so his position is given,+ -- note that the actor may be moved by the effect; the item is destroyed,+ -- if ever, after the discovery happens).+ postb <- getsState $ getActorBody source+ let atomic iid = execCmdAtomic $ DiscoverA (blid postb) (bpos postb) iid ik when b $ maybe skip atomic miid -- | The source actor affects the target actor, with a given effect and power.@@ -84,7 +87,7 @@ Effect.Regeneration p -> effectSem (Effect.Heal p) source target Effect.Searching p -> effectSearching p source Effect.Ascend p -> effectAscend p target- Effect.Escape -> effectEscape target+ Effect.Escape{} -> effectEscape target -- + Individual semantic functions for effects @@ -166,6 +169,7 @@ execSfxAtomic $ EffectD target Effect.Dominate -- TODO: Perhaps insert a turn of delay here to allow countermeasures. electLeader (bfid tb) (blid tb) target+ deduceKilled tb ais <- getsState $ getActorItem target execCmdAtomic $ LoseActorA target tb ais let bNew = tb {bfid = bfid sb}@@ -177,7 +181,6 @@ when (delta > speedZero) $ execCmdAtomic $ HasteActorA target (speedNegate delta) execCmdAtomic $ LeadFactionA (bfid sb) leaderOld (Just target)- deduceKilled tb -- tb (not bNew), because that's how we saw him last return True electLeader :: MonadAtomic m => FactionId -> LevelId -> ActorId -> m ()@@ -195,13 +198,15 @@ deduceKilled :: (MonadAtomic m, MonadServer m) => Actor -> m () deduceKilled body = do- let fid = bfid body+ cops@Kind.COps{corule} <- getsState scops+ let firstDeathEnds = rfirstDeathEnds $ Kind.stdRuleset corule+ fid = bfid body spawn <- getsState $ isSpawnFaction fid- summon <- getsState $ isSummonFaction fid- Config{configFirstDeathEnds} <- getsServer sconfig+ fact <- getsState $ (EM.! fid) . sfactionD+ let horror = isHorrorFact cops fact mleader <- getsState $ gleader . (EM.! fid) . sfactionD- when (not spawn && not summon- && (isNothing mleader || configFirstDeathEnds)) $+ when (not spawn && not horror+ && (isNothing mleader || firstDeathEnds)) $ deduceQuits body $ Status Killed (fromEnum $ blid body) "" -- ** SummonFriend@@ -242,22 +247,28 @@ -> Char -> Text -> Color.Color -> Time -> m ActorId addActor mk bfid pos lid hp bsymbol bname bcolor time = do- Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops- let kind = okind mk+ Kind.COps{coactor=coactor@Kind.Ops{okind}} <- getsState scops+ Faction{gplayer} <- getsState $ (EM.! bfid) . sfactionD+ DebugModeSer{sdifficultySer} <- getsServer sdebugSer+ nU <- nUI+ -- If no UI factions, the difficulty applies to heroes (for testing).+ let diffHP | playerUI gplayer || nU == 0 && mk == heroKindId coactor =+ (ceiling :: Double -> Int) $ fromIntegral hp * 1.5 ^^ sdifficultySer+ | otherwise = hp+ kind = okind mk speed = aspeed kind- m = actorTemplate mk bsymbol bname bcolor speed hp Nothing pos lid time- bfid False+ m = actorTemplate mk bsymbol bname bcolor speed diffHP+ Nothing pos lid time bfid False acounter <- getsServer sacounter modifyServer $ \ser -> ser {sacounter = succ acounter} execCmdAtomic $ CreateActorA acounter m []- return acounter+ return $! acounter --- TODO: apply this special treatment only to actors with symbol '@'. -- | Create a new hero on the current level, close to the given position. addHero :: (MonadAtomic m, MonadServer m) => FactionId -> Point -> LevelId -> [(Int, Text)] -> Maybe Int -> Time -> m ActorId-addHero bfid ppos lid configHeroNames mNumber time = do+addHero bfid ppos lid heroNames mNumber time = do Kind.COps{coactor=coactor@Kind.Ops{okind}} <- getsState scops Faction{gcolor, gplayer} <- getsState $ (EM.! bfid) . sfactionD let kId = heroKindId coactor@@ -266,12 +277,14 @@ let freeHeroK = elemIndex Nothing mhs n = fromMaybe (fromMaybe 100 freeHeroK) mNumber symbol = if n < 1 || n > 9 then '@' else Char.intToDigit n+ nameFromNumber 0 = "Captain"+ nameFromNumber k = "Hero" <+> tshow k name | gcolor == Color.BrWhite =- fromMaybe ("Hero" <+> showT n) $ lookup n configHeroNames- | otherwise = playerName gplayer <+> "Hero" <+> showT n- startHP = hp - (hp `div` 5) * min 3 n- addActor- kId bfid ppos lid startHP symbol name gcolor time+ fromMaybe (nameFromNumber n) $ lookup n heroNames+ | otherwise =+ playerName gplayer <+> nameFromNumber n+ startHP = hp - (min 10 $ hp `div` 10) * min 5 n+ addActor kId bfid ppos lid startHP symbol name gcolor time -- ** SpawnMonster @@ -290,8 +303,9 @@ spawnMonsters (take power ps) (blid tm) time fid return True --- | Spawn monsters of any spawn or summon faction, friendly or not.--- To be used for spontaneous spawning of monsters and for the summon effect.+-- | Spawn non-hero actors of any faction, friendly or not.+-- To be used for initial dungeon population, spontaneous spawning+-- of monsters and for the summon effect. spawnMonsters :: (MonadAtomic m, MonadServer m) => [Point] -> LevelId -> Time -> FactionId -> m ()@@ -352,21 +366,31 @@ discoRev <- getsServer sdiscoRev Level{ldepth, litemFreq} <- getLevel lid depth <- getsState sdepth+ let container = CFloor lid pos replicateM_ n $ do (item, k, _) <- rndToAction $ newItem coitem flavour discoRev litemFreq ldepth depth- itemRev <- getsServer sitemRev- case HM.lookup item itemRev of- Just iid ->- -- TODO: try to avoid this case, to make items more interesting- execCmdAtomic $ CreateItemA iid item k (CFloor lid pos)- Nothing -> do- icounter <- getsServer sicounter- modifyServer $ \ser ->- ser { sicounter = succ icounter- , sitemRev = HM.insert item icounter (sitemRev ser) }- execCmdAtomic $ CreateItemA icounter item k (CFloor lid pos)+ void $ registerItem item k container True +registerItem :: (MonadAtomic m, MonadServer m)+ => Item -> Int -> Container -> Bool -> m ItemId+registerItem item k container verbose = do+ itemRev <- getsServer sitemRev+ let cmd = if verbose then CreateItemA else SpotItemA+ case HM.lookup item itemRev of+ Just iid -> do+ -- TODO: try to avoid this case for createItems,+ -- to make items more interesting+ execCmdAtomic $ cmd iid item k container+ return iid+ Nothing -> do+ icounter <- getsServer sicounter+ modifyServer $ \ser ->+ ser { sicounter = succ icounter+ , sitemRev = HM.insert item icounter (sitemRev ser) }+ execCmdAtomic $ cmd icounter item k container+ return $! icounter+ -- ** ApplyPerfume effectApplyPerfume :: MonadAtomic m@@ -399,56 +423,67 @@ effectAscend :: MonadAtomic m => Int -> ActorId -> m Bool effectAscend power target = do- b <- effLvlGoUp target power- when b $ execSfxAtomic $ EffectD target $ Effect.Ascend power- return b+ mfail <- effLvlGoUp target power+ case mfail of+ Nothing -> do+ execSfxAtomic $ EffectD target $ Effect.Ascend power+ return True+ Just failMsg -> do+ b <- getsState $ getActorBody target+ execSfxAtomic $ MsgFidD (bfid b) failMsg+ return False -effLvlGoUp :: MonadAtomic m => ActorId -> Int -> m Bool+effLvlGoUp :: MonadAtomic m => ActorId -> Int -> m (Maybe Msg) effLvlGoUp aid k = do b1 <- getsState $ getActorBody aid ais1 <- getsState $ getActorItem aid let lid1 = blid b1 pos1 = bpos b1- (lid2, pos2) <- getsState $ whereTo lid1 pos1 k- if lid2 == lid1 && pos2 == pos1 || bproj b1 then- return False+ (lid2, pos2) <- getsState $ whereTo lid1 pos1 k . sdungeon+ if lid2 == lid1 && pos2 == pos1 then+ return $ Just "The effect fizzles: no more levels in this direction."+ else if bproj b1 then+ assert `failure` "projectiles can't exit levels" `twith` (aid, k, b1) else do+ let switch1 = switchLevels1 ((aid, b1), ais1)+ switch2 = do+ -- Move the actor to where the inhabitant was, if any.+ switchLevels2 lid2 pos2 ((aid, b1), ais1)+ -- Verify only one non-projectile actor on every tile.+ !_ <- getsState $ posToActors pos1 lid1 -- assertion is inside+ !_ <- getsState $ posToActors pos2 lid2 -- assertion is inside+ return Nothing -- The actor is added to the new level, but there can be other actors -- at his new position.- inhabitants <- getsState $ posToActor pos2 lid2+ inhabitants <- getsState $ posToActors pos2 lid2 case inhabitants of- Nothing ->- -- Move the actor out of the way.- switchLevels1 aid- Just aid2 -> do- b2 <- getsState $ getActorBody aid2- ais2 <- getsState $ getActorItem aid2+ [] -> do+ switch1+ switch2+ ((_, b2), _) : _ -> do -- Alert about the switch.- let part2 = partActor b2+ let subjects = map (partActor . snd . fst) inhabitants+ subject = MU.WWandW subjects verb = "be pushed to another level"- msg2 = makeSentence [MU.SubjectVerbSg part2 verb]+ msg2 = makeSentence [MU.SubjectVerbSg subject verb]+ -- Only tell one player, even if many actors, because then+ -- they are projectiles, so not too important. execSfxAtomic $ MsgFidD (bfid b2) msg2 -- Move the actor out of the way.- switchLevels1 aid+ switch1 -- Move the inhabitant out of the way.- switchLevels1 aid2+ mapM_ switchLevels1 inhabitants -- Move the inhabitant to where the actor was.- switchLevels2 aid2 b2 ais2 lid1 pos1- -- Move the actor to where the inhabitant was, if any.- switchLevels2 aid b1 ais1 lid2 pos2- -- Verify only one actor on every tile.- !_ <- getsState $ posToActor pos1 lid1 -- assertion is inside- !_ <- getsState $ posToActor pos2 lid2 -- assertion is inside- return True+ mapM_ (switchLevels2 lid1 pos1) inhabitants+ switch2 -switchLevels1 :: MonadAtomic m => ActorId -> m ()-switchLevels1 aid = do- bOld <- getsState $ getActorBody aid- ais <- getsState $ getActorItem aid+switchLevels1 :: MonadAtomic m => ((ActorId, Actor), [(ItemId, Item)]) -> m ()+switchLevels1 ((aid, bOld), ais) = do let side = bfid bOld mleader <- getsState $ gleader . (EM.! side) . sfactionD -- Prevent leader pointing to a non-existing actor.- when (isJust mleader) $ -- trouble, if the actors are of the same faction+ when (not (bproj bOld) && isJust mleader) $+ -- Trouble, if the actors are of the same faction. execCmdAtomic $ LeadFactionA side mleader Nothing -- Remove the actor from the old level. -- Onlookers see somebody disappear suddenly.@@ -456,9 +491,9 @@ execCmdAtomic $ LoseActorA aid bOld ais switchLevels2 :: MonadAtomic m- => ActorId -> Actor -> [(ItemId, Item)] -> LevelId -> Point+ => LevelId -> Point -> ((ActorId, Actor), [(ItemId, Item)]) -> m ()-switchLevels2 aid bOld ais lidNew posNew = do+switchLevels2 lidNew posNew ((aid, bOld), ais) = do let lidOld = blid bOld side = bfid bOld assert (lidNew /= lidOld `blame` "stairs looped" `twith` lidNew) skip@@ -471,10 +506,9 @@ let delta = timeAdd (btime bOld) (timeNegate timeOld) bNew = bOld { blid = lidNew , btime = timeAdd timeLastVisited delta- , bwait = timeZero -- no longer braced , bpos = posNew , boldpos = posNew -- new level, new direction- , bpath = Nothing }+ , boldlid = lidOld } -- record old level mleader <- getsState $ gleader . (EM.! side) . sfactionD -- Materialize the actor at the new location. -- Onlookers see somebody appear suddenly. The actor himself@@ -483,7 +517,8 @@ -- Changing levels is so important, that the leader changes. -- This also helps the actor clear the staircase and so avoid -- being pushed back to the level he came from by another actor.- when (isNothing mleader) $ -- trouble, if the actors are of the same faction+ when (not (bproj bOld) && isNothing mleader) $+ -- Trouble, if the actors are of the same faction. execCmdAtomic $ LeadFactionA side Nothing (Just aid) -- ** Escape@@ -492,11 +527,12 @@ effectEscape :: (MonadAtomic m, MonadServer m) => ActorId -> m Bool effectEscape aid = do -- Obvious effect, nothing announced.+ cops <- getsState scops b <- getsState $ getActorBody aid let fid = bfid b- spawn <- getsState $ isSpawnFaction fid- summon <- getsState $ isSummonFaction fid- if spawn || summon || bproj b then return False+ fact <- getsState $ (EM.! fid) . sfactionD+ if not (isHeroFact cops fact) || bproj b then+ return False else do deduceQuits b $ Status Escape (fromEnum $ blid b) "" return True
Game/LambdaHack/Server/Fov.hs view
@@ -2,15 +2,11 @@ -- See <https://github.com/kosmikus/LambdaHack/wiki/Fov-and-los> -- for discussion. module Game.LambdaHack.Server.Fov- ( dungeonPerception, levelPerception- , fullscan, FovMode(..)+ ( dungeonPerception, levelPerception, fullscan ) where -import Control.Arrow (second)-import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import qualified Data.List as L import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState@@ -22,8 +18,8 @@ import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Vector-import Game.LambdaHack.Common.VectorXY import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Server.Fov.Common import qualified Game.LambdaHack.Server.Fov.Digital as Digital@@ -31,36 +27,41 @@ import qualified Game.LambdaHack.Server.Fov.Shadow as Shadow newtype PerceptionReachable = PerceptionReachable- { preachable :: ES.EnumSet Point }+ {preachable :: [Point]} deriving Show -- | Calculate perception of the level.-levelPerception :: Kind.COps -> State -> FovMode -> FactionId- -> LevelId -> Level+levelPerception :: Kind.COps -> FovMode -> FactionId+ -> LevelId -> Level -> State -> Perception-levelPerception cops@Kind.COps{cotile} s configFov fid lid lvl =- let hs = actorAssocs (== fid) lid s- reas = map (second $ computeReachable cops configFov lvl) hs- lreas = map (preachable . snd) reas- totalRea = PerceptionReachable $ ES.unions lreas+levelPerception cops@Kind.COps{cotile, coactor=Kind.Ops{okind}}+ configFov fid lid lvl@Level{lxsize, lysize} s =+ let hs = actorNotProjList (== fid) lid s+ cR b = preachable $ computeReachable cops configFov lvl b+ totalReachable = PerceptionReachable $ concatMap cR hs -- TODO: give actors light sources explicitly or alter vision.- lights = ES.fromList $ map (bpos . snd) hs- ptotal = computeVisible cotile totalRea lvl lights- g = PerceptionVisible . ES.intersection (pvisible ptotal) . preachable- perActor = EM.map g $ EM.fromList reas -- reas is not sorted- psmell = smellFromActors cops s perActor- in Perception {..}+ pAndVicinity p = p : vicinity lxsize lysize p+ lightsBodies = map (\b -> (pAndVicinity $ bpos b, b)) hs+ light = concat $ map fst lightsBodies+ ptotal = computeVisible cotile totalReachable lvl light+ canSmell b = asmell $ okind $ bkind b+ -- We assume smell FOV radius is always 1, regardless of vision+ -- radius of the actor (if he can see at all).+ psmell = PerceptionVisible $ ES.fromList+ $ concat $ map fst $ filter (canSmell . snd) lightsBodies+ in Perception ptotal psmell -- | Calculate perception of a faction.-factionPerception :: Kind.COps -> FovMode -> State -> FactionId+factionPerception :: Kind.COps -> FovMode -> FactionId -> State -> FactionPers-factionPerception cops configFov s fid =- EM.mapWithKey (levelPerception cops s configFov fid) $ sdungeon s+factionPerception cops configFov fid s =+ EM.mapWithKey (\lid lvl -> levelPerception cops configFov fid lid lvl s)+ $ sdungeon s -- | Calculate the perception of the whole dungeon. dungeonPerception :: Kind.COps -> FovMode -> State -> Pers dungeonPerception cops configFov s =- let f fid _ = factionPerception cops configFov s fid+ let f fid _ = factionPerception cops configFov fid s in EM.mapWithKey f $ sfactionD s -- | A position can be directly lit by an ambient shine or a weak, portable@@ -78,24 +79,13 @@ -- there must be a wall in-between. Stray rays indicate doors, -- moving shadows indicate monsters, etc. computeVisible :: Kind.Ops TileKind -> PerceptionReachable- -> Level -> ES.EnumSet Point -> PerceptionVisible-computeVisible cotile reachable@PerceptionReachable{preachable} lvl lights =- let isV = isVisible cotile reachable lvl lights- in PerceptionVisible $ ES.filter isV preachable---- TODO: this is calculated per-faction, not per-actor, but still optimize,--- e.g., by partitioning preachable wrt litDirectly and then running--- isVisible only over one of the parts, depending on which is smaller.-isVisible :: Kind.Ops TileKind -> PerceptionReachable- -> Level -> ES.EnumSet Point -> Point -> Bool-isVisible cotile PerceptionReachable{preachable}- lvl@Level{lxsize, lysize} lights pos0 =- let litDirectly pos = Tile.isLit cotile (lvl `at` pos)- || pos `ES.member` lights- l_and_R pos = litDirectly pos && pos `ES.member` preachable- in litDirectly pos0 || L.any l_and_R (vicinity lxsize lysize pos0)+ -> Level -> [Point] -> PerceptionVisible+computeVisible cotile PerceptionReachable{preachable} lvl lights =+ let isVisible pos = Tile.isLit cotile (lvl `at` pos)+ in PerceptionVisible $ ES.fromList $ lights ++ filter isVisible preachable --- | Reachable are all fields on an unblocked path from the hero position.+-- | Reachable are all fields on a visually unblocked path+-- from the hero position. computeReachable :: Kind.COps -> FovMode -> Level -> Actor -> PerceptionReachable computeReachable Kind.COps{cotile, coactor=Kind.Ops{okind}}@@ -104,7 +94,7 @@ fovMode = if sight then configFov else Blind ppos = bpos body scan = fullscan cotile fovMode ppos lvl- in PerceptionReachable $ ES.fromList scan+ in PerceptionReachable scan -- | Perform a full scan for a given position. Returns the positions -- that are currently in the field of view. The Field of View@@ -115,65 +105,45 @@ -> Point -- ^ position of the spectator -> Level -- ^ the map that is scanned -> [Point]-fullscan cotile fovMode spectatorPos Level{lxsize, ltile} = spectatorPos :+fullscan cotile fovMode spectatorPos lvl = spectatorPos : case fovMode of Shadow ->- L.concatMap (\ tr -> map tr (Shadow.scan (isCl . tr) 1 (0, 1))) tr8+ concatMap (\tr -> map tr (Shadow.scan (isCl . tr) 1 (0, 1))) tr8 Permissive ->- L.concatMap (\ tr -> map tr (Permissive.scan (isCl . tr))) tr4+ concatMap (\tr -> map tr (Permissive.scan (isCl . tr))) tr4 Digital r ->- L.concatMap (\ tr -> map tr (Digital.scan r (isCl . tr))) tr4+ concatMap (\tr -> map tr (Digital.scan r (isCl . tr))) tr4 Blind -> -- all actors feel adjacent positions (for easy exploration) let radiusOne = 1- in L.concatMap (\ tr -> map tr (Digital.scan radiusOne (isCl . tr))) tr4+ in concatMap (\tr -> map tr (Digital.scan radiusOne (isCl . tr))) tr4 where isCl :: Point -> Bool- isCl = Tile.isClear cotile . (ltile Kind.!)+ isCl = Tile.isClear cotile . (lvl `at`) - trV xy = shift spectatorPos $ toVector lxsize $ VectorXY xy+ -- This function is cheap, so no problem it's called twice+ -- for each point: once with @isCl@, once via @concatMap@.+ trV :: X -> Y -> Point+ {-# INLINE trV #-}+ trV x y = shift spectatorPos $ Vector x y -- | The translation, rotation and symmetry functions for octants. tr8 :: [(Distance, Progress) -> Point] tr8 =- [ \ (p, d) -> trV ( p, d)- , \ (p, d) -> trV (- p, d)- , \ (p, d) -> trV ( p, - d)- , \ (p, d) -> trV (- p, - d)- , \ (p, d) -> trV ( d, p)- , \ (p, d) -> trV (- d, p)- , \ (p, d) -> trV ( d, - p)- , \ (p, d) -> trV (- d, - p)+ [ \(p, d) -> trV p d+ , \(p, d) -> trV (-p) d+ , \(p, d) -> trV p (-d)+ , \(p, d) -> trV (-p) (-d)+ , \(p, d) -> trV d p+ , \(p, d) -> trV (-d) p+ , \(p, d) -> trV d (-p)+ , \(p, d) -> trV (-d) (-p) ] -- | The translation and rotation functions for quadrants. tr4 :: [Bump -> Point] tr4 =- [ \ (B(x, y)) -> trV ( x, - y) -- quadrant I- , \ (B(x, y)) -> trV ( y, x) -- II (we rotate counter-clockwise)- , \ (B(x, y)) -> trV (- x, y) -- III- , \ (B(x, y)) -> trV (- y, - x) -- IV+ [ \B{..} -> trV bx (-by) -- quadrant I+ , \B{..} -> trV by bx -- II (we rotate counter-clockwise)+ , \B{..} -> trV (-bx) by -- III+ , \B{..} -> trV (-by) (-bx) -- IV ]---- 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)--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)"
Game/LambdaHack/Server/Fov/Common.hs view
@@ -7,14 +7,12 @@ -- * Scanning coordinate system , Bump(..) -- * Geometry in system @Bump@- , Line, ConvexHull, Edge, EdgeInterval+ , Line(..), ConvexHull, Edge, EdgeInterval -- * Assorted minor operations , maximal, steeper, addHull ) where -import qualified Data.List as L--import Game.LambdaHack.Common.PointXY+import Data.List -- | Distance from the (0, 0) point where FOV originates. type Distance = Int@@ -28,11 +26,16 @@ -- The special coordinates are written using the standard mathematical -- coordinate setup, where quadrant I, with x and y positive, -- is on the upper right.-newtype Bump = B (X, Y)- deriving (Show)+data Bump = B+ { bx :: !Int+ , by :: !Int+ }+ deriving Show -- | Straight line between points.-type Line = (Bump, Bump)+data Line = Line !Bump !Bump+ deriving Show+ -- | Convex hull represented as a list of points. type ConvexHull = [Bump] -- | An edge (comprising of a line and a convex hull)@@ -44,14 +47,16 @@ -- | Maximal element of a non-empty list. Prefers elements from the rear, -- which is essential for PFOV, to avoid ill-defined lines. maximal :: (a -> a -> Bool) -> [a] -> a-maximal gte = L.foldl1' (\ acc e -> if gte e acc then e else acc)+{-# INLINE maximal #-}+maximal gte = foldl1' (\acc e -> if gte e acc then e else acc) -- | Check if the line from the second point to the first is more steep -- than the line from the third point to the first. This is related -- to the formal notion of gradient (or angle), but hacked wrt signs -- to work fast in this particular setup. Returns True for ill-defined lines. steeper :: Bump -> Bump -> Bump -> Bool-steeper (B(xf, yf)) (B(x1, y1)) (B(x2, y2)) =+{-# INLINE steeper #-}+steeper (B xf yf) (B x1 y1) (B x2 y2) = (yf - y1)*(xf - x2) >= (yf - y2)*(xf - x1) -- | Extends a convex hull of bumps with a new bump. Nothing needs to be done@@ -61,6 +66,7 @@ -> Bump -- ^ a new bump to consider -> ConvexHull -- ^ a convex hull of bumps represented as a list -> ConvexHull+{-# INLINE addHull #-} addHull gte new = (new :) . go where go (a:b:cs) | gte a b = go (b:cs)
Game/LambdaHack/Server/Fov/Digital.hs view
@@ -5,9 +5,10 @@ ( scan, dline, dsteeper, intersect, debugSteeper, debugLine ) where +import Control.Exception.Assert.Sugar+ import Game.LambdaHack.Common.Misc import Game.LambdaHack.Server.Fov.Common-import Control.Exception.Assert.Sugar -- | Calculates the list of tiles, in @Bump@ coordinates, visible from (0, 0), -- within the given sight range.@@ -16,10 +17,13 @@ -> [Bump] scan r isClear = -- The scanned area is a square, which is a sphere in the chessboard metric.- dscan 1 (((B(1, 0), B(-r, r)), [B(0, 0)]), ((B(0, 0), B(r+1, r)), [B(1, 0)]))+ dscan 1 ( (Line (B 1 0) (B (-r) r), [B 0 0])+ , (Line (B 0 0) (B (r+1) r), [B 1 0]) ) where dscan :: Distance -> EdgeInterval -> [Bump]- dscan d (s0@(sl{-shallow line-}, sBumps0), e@(el{-steep line-}, eBumps)) =+ dscan d ( s0@(sl{-shallow line-}, sHull0)+ , e@(el{-steep line-}, eHull) ) =+ let ps0 = let (n, k) = intersect sl d -- minimal progress to consider in n `div` k pe = let (n, k) = intersect el d -- maximal progress to consider@@ -28,49 +32,53 @@ -- so if its intersection with the line of diagonals is only -- at a corner, choose the diamond leading to a smaller view. in -1 + n `divUp` k- inside = [B(p, d) | p <- [ps0..pe]]+ inside = [B p d | p <- [ps0..pe]] outside | d >= r = []- | isClear (B(ps0, d)) = mscan (Just s0) (ps0+1) pe -- start in light- | otherwise = mscan Nothing (ps0+1) pe -- start in shadow+ | isClear (B ps0 d) = mscanVisible s0 (ps0+1) -- start visible+ | otherwise = mscanShadowed (ps0+1) -- start in shadow++ -- We're in a visible interval.+ mscanVisible :: Edge -> Progress -> [Bump]+ mscanVisible s@(_, sHull) ps+ | ps > pe = dscan (d+1) (s, e) -- reached end, scan next+ | not $ isClear steepBump = -- entering shadow+ mscanShadowed (ps+1)+ ++ dscan (d+1) (s, (dline nep steepBump, neHull))+ | otherwise = mscanVisible s (ps+1) -- continue in visible area+ where+ steepBump = B ps d+ gte = dsteeper steepBump+ nep = maximal gte sHull+ neHull = addHull gte steepBump eHull++ -- We're in a shadowed interval.+ mscanShadowed :: Progress -> [Bump]+ mscanShadowed ps+ | ps > pe = [] -- reached end while in shadow+ | isClear shallowBump = -- moving out of shadow+ mscanVisible (dline nsp shallowBump, nsHull) (ps+1)+ | otherwise = mscanShadowed (ps+1) -- continue in shadow+ where+ shallowBump = B ps d+ gte = flip $ dsteeper shallowBump+ nsp = maximal gte eHull+ nsHull = addHull gte shallowBump sHull0+ in assert (r >= d && d >= 0 && pe >= ps0 `blame` (r,d,s0,e,ps0,pe)) $ inside ++ outside- where- -- The current state of a scan is kept in @Maybe Edge@.- -- If it's the @Just@ case, we're in a visible interval. If @Nothing@,- -- we're in a shadowed interval.- mscan :: Maybe Edge -> Progress -> Progress -> [Bump]- mscan (Just s@(_, sBumps)) ps pe- | ps > pe = dscan (d+1) (s, e) -- reached end, scan next- | not $ isClear steepBump = -- entering shadow- mscan Nothing (ps+1) pe- ++ dscan (d+1) (s, (dline nep steepBump, neBumps))- | otherwise = mscan (Just s) (ps+1) pe -- continue in light- where- steepBump = B(ps, d)- gte = dsteeper steepBump- nep = maximal gte sBumps- neBumps = addHull gte steepBump eBumps - mscan Nothing ps pe- | ps > pe = [] -- reached end while in shadow- | isClear shallowBump = -- moving out of shadow- mscan (Just (dline nsp shallowBump, nsBumps)) (ps+1) pe- | otherwise = mscan Nothing (ps+1) pe -- continue in shadow- where- shallowBump = B(ps, d)- gte = flip $ dsteeper shallowBump- nsp = maximal gte eBumps- nsBumps = addHull gte shallowBump sBumps0- -- | Create a line from two points. Debug: check if well-defined. dline :: Bump -> Bump -> Line+{-# INLINE dline #-} dline p1 p2 =- assert (uncurry blame $ debugLine (p1, p2)) (p1, p2)+ let line = Line p1 p2+ in assert (uncurry blame $ debugLine line) line -- | Compare steepness of @(p1, f)@ and @(p2, f)@. -- Debug: Verify that the results of 2 independent checks are equal. dsteeper :: Bump -> Bump -> Bump -> Bool+{-# INLINE dsteeper #-} dsteeper f p1 p2 = assert (res == debugSteeper f p1 p2) res where res = steeper f p1 p2@@ -79,7 +87,8 @@ -- a given line and the line of diagonals of diamonds at distance -- @d@ from (0, 0). intersect :: Line -> Distance -> (Int, Int)-intersect (B(x, y), B(xf, yf)) d =+{-# INLINE intersect #-}+intersect (Line (B x y) (B xf yf)) d = assert (allB (>= 0) [y, yf]) ((d - y)*(xf - x) + x*(yf - y), yf - y) {-@@ -115,15 +124,17 @@ -- | Debug: calculate steeper for DFOV in another way and compare results. debugSteeper :: Bump -> Bump -> Bump -> Bool-debugSteeper f@(B(_xf, yf)) p1@(B(_x1, y1)) p2@(B(_x2, y2)) =+{-# INLINE debugSteeper #-}+debugSteeper f@(B _xf yf) p1@(B _x1 y1) p2@(B _x2 y2) = assert (allB (>= 0) [yf, y1, y2]) $- let (n1, k1) = intersect (p1, f) 0- (n2, k2) = intersect (p2, f) 0+ let (n1, k1) = intersect (Line p1 f) 0+ (n2, k2) = intersect (Line p2 f) 0 in n1 * k2 >= k1 * n2 -- | Debug: check if a view border line for DFOV is legal. debugLine :: Line -> (Bool, String)-debugLine line@(B(x1, y1), B(x2, y2))+{-# INLINE debugLine #-}+debugLine line@(Line (B x1 y1) (B x2 y2)) | not (allB (>= 0) [y1, y2]) = (False, "negative coordinates: " ++ show line) | y1 == y2 && x1 == x2 =
Game/LambdaHack/Server/Fov/Permissive.hs view
@@ -7,9 +7,10 @@ ( scan, dline, dsteeper, intersect, debugSteeper, debugLine ) where +import Control.Exception.Assert.Sugar+ import Game.LambdaHack.Common.Misc import Game.LambdaHack.Server.Fov.Common-import Control.Exception.Assert.Sugar -- TODO: Scanning squares on horizontal lines in octants, not squares -- on diagonals in quadrants, may be much faster and a bit simpler.@@ -20,10 +21,12 @@ scan :: (Bump -> Bool) -- ^ clear tile predicate -> [Bump] scan isClear =- dscan 1 (((B(0, 1), B(999, 0)), [B(1, 0)]), ((B(1, 0), B(0, 999)), [B(0, 1)]))+ dscan 1 ( (Line (B 0 1) (B 999 0), [B 1 0])+ , (Line (B 1 0) (B 0 999), [B 0 1]) ) where dscan :: Distance -> EdgeInterval -> [Bump]- dscan d (s0@(sl{-shallow line-}, sBumps0), e@(el{-steep line-}, eBumps)) =+ dscan d ( s0@(sl{-shallow line-}, sHull0)+ , e@(el{-steep line-}, eHull) ) = assert (d >= 0 && pe + 1 >= ps0 && ps0 >= 0 `blame` (d,s0,e,ps0,pe)) $ if illegal then [] else inside ++ outside@@ -36,46 +39,47 @@ -- A single ray from an extremity produces non-permissive digital lines. illegal = let (n, k) = intersect sl 0 in ns*ke == ne*ks && (n `elem` [0, k])- pd2bump (p, di) = B(di - p , p)- bottomRight (p, di) = B(di - p + 1, p)+ pd2bump (p, di) = B (di - p) p+ bottomRight (p, di) = B (di - p + 1) p inside = [pd2bump (p, d) | p <- [ps0..pe]] outside- | isClear (pd2bump (ps0, d)) = mscan (Just s0) ps0 -- start in light- | ps0 == ns `divUp` ks = mscan (Just s0) ps0 -- start in a corner- | otherwise = mscan Nothing (ps0+1) -- start in mid-wall+ | isClear (pd2bump (ps0, d)) = mscanVisible s0 ps0 -- start visible+ | ps0 == ns `divUp` ks = mscanVisible s0 ps0 -- start in a corner+ | otherwise = mscanShadowed (ps0+1) -- start in mid-wall - -- The current state of a scan is kept in @Maybe Edge@.- -- If it's the @Just@ case, we're in a visible interval. If @Nothing@,- -- we're in a shadowed interval.- mscan :: Maybe Edge -> Progress -> [Bump]- mscan (Just s@(_, sBumps)) ps+ -- We're in a visible interval.+ mscanVisible :: Edge -> Progress -> [Bump]+ mscanVisible s@(_, sHull) ps | ps > pe = dscan (d+1) (s, e) -- reached end, scan next | not $ isClear (pd2bump (ps, d)) = -- enter shadow, steep bump let steepBump = bottomRight (ps, d) gte = flip $ dsteeper steepBump- -- sBumps may contain steepBump, but maximal will ignore it- nep = maximal gte sBumps- neBumps = addHull gte steepBump eBumps- in mscan Nothing (ps+1)- ++ dscan (d+1) (s, (dline nep steepBump, neBumps))- | otherwise = mscan (Just s) (ps+1) -- continue in light+ -- sHull may contain steepBump, but maximal will ignore it+ nep = maximal gte sHull+ neHull = addHull gte steepBump eHull+ in mscanShadowed (ps+1)+ ++ dscan (d+1) (s, (dline nep steepBump, neHull))+ | otherwise = mscanVisible s (ps+1) -- continue in visible area - mscan Nothing ps+ -- we're in a shadowed interval.+ mscanShadowed :: Progress -> [Bump]+ mscanShadowed ps | ps > ne `div` ke = [] -- reached absolute end | otherwise = -- out of shadow, shallow bump- -- the light can be just through a corner of diagonal walls+ -- the ray can just pass through a corner of diagonal walls -- and the recursive call verifies that at the same ps coordinate let shallowBump = bottomRight (ps, d) gte = dsteeper shallowBump- nsp = maximal gte eBumps- nsBumps = addHull gte shallowBump sBumps0- in mscan (Just (dline nsp shallowBump, nsBumps)) ps+ nsp = maximal gte eHull+ nsHull = addHull gte shallowBump sHull0+ in mscanVisible (dline nsp shallowBump, nsHull) ps -- | Create a line from two points. Debug: check if well-defined. dline :: Bump -> Bump -> Line dline p1 p2 =- assert (uncurry blame $ debugLine (p1, p2)) (p1, p2)+ let line = Line p1 p2+ in assert (uncurry blame $ debugLine line) line -- | Compare steepness of @(p1, f)@ and @(p2, f)@. -- Debug: Verify that the results of 2 independent checks are equal.@@ -88,7 +92,7 @@ -- a given line and the line of diagonals of squares at distance -- @d@ from (0, 0). intersect :: Line -> Distance -> (Int, Int)-intersect (B(x, y), B(xf, yf)) d =+intersect (Line (B x y) (B xf yf)) d = assert (allB (>= 0) [x, y, xf, yf]) ((1 + d)*(yf - y) + y*xf - x*yf, (xf - x) + (yf - y)) {-@@ -125,15 +129,15 @@ -- | Debug: calculate steeper for PFOV in another way and compare results. debugSteeper :: Bump -> Bump -> Bump -> Bool-debugSteeper f@(B(xf, yf)) p1@(B(x1, y1)) p2@(B(x2, y2)) =+debugSteeper f@(B xf yf) p1@(B x1 y1) p2@(B x2 y2) = assert (allB (>= 0) [xf, yf, x1, y1, x2, y2]) $- let (n1, k1) = intersect (p1, f) 0- (n2, k2) = intersect (p2, f) 0+ let (n1, k1) = intersect (Line p1 f) 0+ (n2, k2) = intersect (Line p2 f) 0 in n1 * k2 <= k1 * n2 -- | Debug: checks postconditions of borderLine. debugLine :: Line -> (Bool, String)-debugLine line@(B(x1, y1), B(x2, y2))+debugLine line@(Line (B x1 y1) (B x2 y2)) | not (allB (>= 0) [x1, y1, x2, y2]) = (False, "negative coordinates: " ++ show line) | y1 == y2 && x1 == x2 =
Game/LambdaHack/Server/Fov/Shadow.hs view
@@ -4,10 +4,10 @@ -- The main advantage of the algorithm is that it's very simple and fast. module Game.LambdaHack.Server.Fov.Shadow (SBump, Interval, scan) where +import Control.Exception.Assert.Sugar import Data.Ratio import Game.LambdaHack.Server.Fov.Common-import Control.Exception.Assert.Sugar {- Field Of View
Game/LambdaHack/Server/LoopAction.hs view
@@ -3,16 +3,15 @@ module Game.LambdaHack.Server.LoopAction (loopSer) where import Control.Arrow ((&&&))+import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import Data.Key (mapWithKeyM_) import Data.List import Data.Maybe import qualified Data.Ord as Ord+import Data.Text (Text) -import Control.Exception.Assert.Sugar-import qualified Game.LambdaHack.Common.Ability as Ability import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState@@ -31,16 +30,16 @@ import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ModeKind+import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Frontend import Game.LambdaHack.Server.Action hiding (sendUpdateAI, sendUpdateUI)-import Game.LambdaHack.Server.Config import Game.LambdaHack.Server.EffectSem import Game.LambdaHack.Server.Fov import Game.LambdaHack.Server.ServerSem import Game.LambdaHack.Server.StartAction import Game.LambdaHack.Server.State+import Game.LambdaHack.Utils.Frequency -- | Start a game session. Loop, communicating with clients. loopSer :: (MonadAtomic m, MonadConnServer m)@@ -48,7 +47,7 @@ -> (CmdSer -> m Bool) -> (FactionId -> ChanFrontend -> ChanServer CmdClientUI CmdSer -> IO ())- -> (FactionId -> ChanServer CmdClientAI CmdSerTakeTime+ -> (FactionId -> ChanServer CmdClientAI CmdTakeTimeSer -> IO ()) -> Kind.COps -> m ()@@ -61,7 +60,7 @@ let setPreviousCops = const cops execCmdAtomic $ ResumeServerA $ updateCOps setPreviousCops sRaw putServer ser- sdebugNxt <- initDebug sdebug+ sdebugNxt <- initDebug cops sdebug modifyServer $ \ser2 -> ser2 {sdebugNxt} applyDebug updateConn executorUI executorAI@@ -72,21 +71,22 @@ let setCurrentCops = const (speedupCOps (sallClear sdebugNxt) cops) -- @sRaw@ is correct here, because none of the above changes State. execCmdAtomic $ ResumeServerA $ updateCOps setCurrentCops sRaw- initPer _ -> do -- Starting a new game. -- Set up commandline debug mode let mrandom = case restored of Just (_, ser) -> Just $ srandom ser Nothing -> Nothing s <- gameReset cops sdebug mrandom- sdebugNxt <- initDebug sdebug- modifyServer $ \ser -> ser {sdebugNxt, sdebugSer = sdebugNxt}+ sdebugNxt <- initDebug cops sdebug+ let debugBarRngs = sdebugNxt {sdungeonRng = Nothing, smainRng = Nothing}+ modifyServer $ \ser -> ser { sdebugNxt = debugBarRngs+ , sdebugSer = debugBarRngs } let speedup = speedupCOps (sallClear sdebugNxt) execCmdAtomic $ RestartServerA $ updateCOps speedup s updateConn executorUI executorAI initPer reinitGame- when (sdumpConfig sdebug) $ void $ dumpCfg+ when (sdumpInitRngs sdebug) $ dumpRngs resetSessionStart -- Start a clip (a part of a turn for which one or more frames -- will be generated). Do whatever has to be done@@ -101,7 +101,7 @@ Just leader -> do b <- getsState $ getActorBody leader return $ Just $ blid b- _ -> return Nothing+ Nothing -> return Nothing factionD <- getsState sfactionD marenas <- mapM factionArena $ EM.elems factionD let arenas = ES.toList $ ES.fromList $ catMaybes marenas@@ -118,34 +118,23 @@ when continue loop loop -initDebug :: MonadServer m => DebugModeSer -> m DebugModeSer-initDebug sdebugSer = do- sconfig <- getsServer sconfig- return $+initDebug :: MonadActionRO m => Kind.COps -> DebugModeSer -> m DebugModeSer+initDebug Kind.COps{corule} sdebugSer = do+ let stdRuleset = Kind.stdRuleset corule+ return $! (\dbg -> dbg {sfovMode =- sfovMode dbg `mplus` Just (configFovMode sconfig)}) .+ sfovMode dbg `mplus` Just (rfovMode stdRuleset)}) . (\dbg -> dbg {ssavePrefixSer =- ssavePrefixSer dbg `mplus` Just (configSavePrefix sconfig)})+ ssavePrefixSer dbg `mplus` Just (rsavePrefix stdRuleset)}) $ sdebugSer --- This can be improved by adding a timeout and by asking clients to prepare--- a save (in this way checking they have permissions, enough space, etc.)--- and when all report back, asking them to commit the save.--- | Save game on server and all clients. Clients are pinged first,--- which greatly reduced the chance of saves being out of sync.-saveBkpAll :: (MonadAtomic m, MonadServer m, MonadConnServer m) => m ()-saveBkpAll = do- factionD <- getsState sfactionD- let ping fid _ = do- sendPingAI fid- when (playerUI $ gplayer $ factionD EM.! fid) $ sendPingUI fid- mapWithKeyM_ ping factionD- execCmdAtomic SaveBkpA- saveServer- endClip :: (MonadAtomic m, MonadServer m, MonadConnServer m) => [LevelId] -> m Bool endClip arenas = do+ Kind.COps{corule} <- getsState scops+ let stdRuleset = Kind.stdRuleset corule+ saveBkpClips = rsaveBkpClips stdRuleset+ leadLevelClips = rleadLevelClips stdRuleset -- TODO: a couple messages each clip to many clients is too costly. -- Store these on a queue and sum times instead of sending, -- until a different command needs to be sent. Include HealActorA@@ -155,15 +144,15 @@ execCmdAtomic $ AgeGameA timeClip -- Perform periodic dungeon maintenance. time <- getsState stime- Config{configSaveBkpClips} <- getsServer sconfig let clipN = time `timeFit` timeClip- cinT = let r = timeTurn `timeFit` timeClip- in assert (r > 2) r- clipMod = clipN `mod` cinT+ clipInTurn = let r = timeTurn `timeFit` timeClip+ in assert (r > 2) r+ clipMod = clipN `mod` clipInTurn bkpSave <- getsServer sbkpSave- when (bkpSave || clipN `mod` configSaveBkpClips == 0) $ do+ when (bkpSave || clipN `mod` saveBkpClips == 0) $ do modifyServer $ \ser -> ser {sbkpSave = False}- saveBkpAll+ saveBkpAll False+ when (clipN `mod` leadLevelClips == 0) leadLevelFlip -- Regenerate HP and add monsters each turn, not each clip. -- Do this on only one of the arenas to prevent micromanagement, -- e.g., spreading leaders across levels to bump monster generation.@@ -171,12 +160,13 @@ arena <- rndToAction $ oneOf arenas regenerateLevelHP arena generateMonster arena- sstopAfter <- getsServer $ sstopAfter . sdebugSer- case sstopAfter of+ stopAfter <- getsServer $ sstopAfter . sdebugSer+ case stopAfter of Nothing -> return True- Just stopAfter -> do- exit <- elapsedSessionTimeGT stopAfter+ Just stopA -> do+ exit <- elapsedSessionTimeGT stopA if exit then do+ tellAllClipPS saveAndExit return False -- don't re-enter the game loop else return True@@ -192,7 +182,6 @@ -> LevelId -> m () handleActors cmdSerSem lid = do- Kind.COps{cofaction=Kind.Ops{okind}} <- getsState scops time <- getsState $ getLocalTime lid -- the end of this clip, inclusive Level{lprio} <- getLevel lid quit <- getsServer squit@@ -212,28 +201,34 @@ case mnext of _ | quit -> return () Nothing -> return ()- Just (aid, b) | bproj b && bhp b < 0 -> do+ Just (aid, b) | bhp b < 0 && bproj b -> do -- A projectile hits an actor. The carried item is destroyed.- -- TODO: perhaps don't destroy if no effect (NoEffect).- ais <- getsState $ getActorItem aid- execCmdAtomic $ DestroyActorA aid b ais+ -- TODO: perhaps don't destroy if no effect (NoEffect),+ -- to help testing items. But OTOH, we want most items to have+ -- some effect, even silly, for flavour. Anyway, if the silly+ -- effect identifies an item, the hit is not wasted, so this makes sense.+ dieSer aid b True -- The attack animation for the projectile hit subsumes @DisplayPushD@, -- so not sending an extra @DisplayPushD@ here. handleActors cmdSerSem lid- Just (aid, b) | bhp b <= 0 && not (bproj b)- || maybe False null (bpath b) -> do- -- An actor (projectile or not) ceases to exist.- -- Items drop to the ground and possibly a new leader is elected.- dieSer aid- -- If it's a death, not a projectile drop, the death animation- -- subsumes @DisplayPushD@, so not sending it here. ProjectileProjectile- -- destruction is not important enough for an extra @DisplayPushD@.+ Just (aid, b) | maybe False null (btrajectory b) -> do+ assert (bproj b) skip+ execSfxAtomic $ DisplayPushD (bfid b) -- show last position before drop+ -- A projectile drops to the ground due to obstacles or range.+ dieSer aid b False handleActors cmdSerSem lid+ Just (aid, b) | bhp b <= 0 && not (bproj b) -> do+ -- An actor dies. Items drop to the ground+ -- and possibly a new leader is elected.+ dieSer aid b False+ -- The death animation subsumes @DisplayPushD@, so not sending it here.+ handleActors cmdSerSem lid Just (aid, body) -> do let side = bfid body fact = factionD EM.! side mleader = gleader fact- queryUI | Just aid == mleader = not $ playerAiLeader $ gplayer fact+ aidIsLeader = mleader == Just aid+ queryUI | aidIsLeader = not $ playerAiLeader $ gplayer fact | otherwise = not $ playerAiOther $ gplayer fact switchLeader cmdS = do -- TODO: check that the command is legal first, report and reject,@@ -245,9 +240,11 @@ then -- Only a leader can change his faction's leader -- before the action is performed (e.g., via AI -- switching leaders). Then, the action can change- -- the leader again (e.g., via using stairs).- assert (mleader == Just aid && not (bproj bPre)- `blame` (aid, aidNew, bPre, cmdS, fact))+ -- the leader again (e.g., via killing the old leader).+ assert (aidIsLeader+ && not (bproj bPre)+ && not (isSpawnFact fact)+ `blame` (aid, body, aidNew, bPre, cmdS, fact)) [LeadFactionA side mleader (Just aidNew)] else [] mapM_ execCmdAtomic leadAtoms@@ -255,6 +252,12 @@ `blame` "client tries to move other faction actors" `twith` (bPre, side)) skip return (aidNew, bPre)+ setBWait (CmdTakeTimeSer WaitSer{}) aidNew bPre = do+ let fromWait = bwait bPre+ unless fromWait $ execCmdAtomic $ WaitActorA aidNew fromWait True+ setBWait _ aidNew bPre = do+ let fromWait = bwait bPre+ when fromWait $ execCmdAtomic $ WaitActorA aidNew fromWait False extraFrames bPre = do -- Generate extra frames if the actor has already moved during -- this clip, so his multiple moves would be collapsed@@ -265,7 +268,22 @@ lastSingleMove = timeAddFromSpeed bPre previousClipEnd when (btime bPre > lastSingleMove) $ broadcastSfxAtomic DisplayPushD- if queryUI then do+ if bproj body then do -- TODO: perhaps check Track, not bproj+ execSfxAtomic $ DisplayPushD side+ let cmdS = CmdTakeTimeSer $ SetTrajectorySer aid+ timed <- cmdSerSem cmdS+ assert timed skip+ b <- getsState $ getActorBody aid+ -- Colliding with a wall or actor doesn't take time, because+ -- the projectile does not move (the move is blocked).+ -- Not advancing time forces dead projectiles to be destroyed ASAP.+ -- Otherwise it would be displayed in the same place twice.+ -- If ever needed this can be implemented properly by moving+ -- SetTrajectorySer out of CmdTakeTimeSer.+ unless (bhp b < 0 || maybe False null (btrajectory b)) $ do+ advanceTime aid+ extraFrames b+ else if queryUI then do -- The client always displays a frame in this case. cmdS <- sendQueryUI side aid (aidNew, bPre) <- switchLeader cmdS@@ -275,6 +293,7 @@ $ MsgFidD side "You strain, fumble and faint from the exertion." return False else cmdSerSem cmdS+ setBWait cmdS aidNew bPre -- Advance time once, after the leader switched perhaps many times. -- TODO: this is correct only when all heroes have the same -- speed and can't switch leaders by, e.g., aiming a wand@@ -292,54 +311,112 @@ -- but one by one. execSfxAtomic $ DisplayPushD side -- Clear messages in the UI client (if any), if the actor- -- is freely moving.- let factionAbilities- | Just aid == mleader = fAbilityLeader $ okind $ gkind fact- | otherwise = fAbilityOther $ okind $ gkind fact- canMove = playerUI (gplayer fact)- && not (bproj body)- && (Ability.Chase `elem` factionAbilities- || Ability.Wander `elem` factionAbilities)- when canMove $ execSfxAtomic $ RecordHistoryD side- cmdT <- sendQueryAI side aid- let cmdS = TakeTimeSer cmdT+ -- is a leader (which happens when a UI client is fully+ -- computer-controlled). We could record history more often,+ -- to avoid long reports, but we'd have to add -more- prompts.+ let mainUIactor = playerUI (gplayer fact) && aidIsLeader+ when mainUIactor $ execSfxAtomic $ RecordHistoryD side+ cmdTimed <- sendQueryAI side aid+ let cmdS = CmdTakeTimeSer cmdTimed (aidNew, bPre) <- switchLeader cmdS assert (not (bhp bPre <= 0 && not (bproj bPre)) `blame` "AI switches to an incapacitated actor" `twith` (cmdS, bPre, side)) skip- void $ cmdSerSem cmdS+ timed <- cmdSerSem cmdS+ assert timed skip+ setBWait cmdS aidNew bPre -- AI always takes time and so doesn't loop. advanceTime aidNew extraFrames bPre handleActors cmdSerSem lid -dieSer :: (MonadAtomic m, MonadServer m) => ActorId -> m ()-dieSer aid = do -- TODO: explode if a projectile holding a potion- body <- getsState $ getActorBody aid+dieSer :: (MonadAtomic m, MonadServer m) => ActorId -> Actor -> Bool -> m ()+dieSer aid b hit = do -- TODO: clients don't see the death of their last standing actor; -- modify Draw.hs and Client.hs to handle that- electLeader (bfid body) (blid body) aid- dropAllItems aid body- execCmdAtomic $ DestroyActorA aid body {bbag = EM.empty} []- deduceKilled body+ if bproj b then do+ dropAllItems aid b hit+ b2 <- getsState $ getActorBody aid+ execCmdAtomic $ DestroyActorA aid b2 []+ else do+ electLeader (bfid b) (blid b) aid+ deduceKilled b+ dropAllItems aid b False+ b2 <- getsState $ getActorBody aid+ execCmdAtomic $ DestroyActorA aid b2 [] --- | Drop all actor's items.-dropAllItems :: MonadAtomic m => ActorId -> Actor -> m ()-dropAllItems aid b = do- let f iid k = execCmdAtomic- $ MoveItemA iid k (actorContainer aid (binv b) iid)- (CFloor (blid b) (bpos b))+-- | Drop all actor's items. If the actor hits another actor and this+-- collision results in all item being dropped, all items are destroyed.+-- If the actor does not hit, but dies, only fragile items are destroyed+-- and only if the actor was a projectile (and so died by dropping+-- to the ground due to exceeded range or bumping off an obstacle).+dropAllItems :: (MonadAtomic m, MonadServer m)+ => ActorId -> Actor -> Bool -> m ()+dropAllItems aid b hit = do+ Kind.COps{coitem} <- getsState scops+ discoS <- getsServer sdisco+ let isDestroyed item = hit || bproj b && isFragile coitem discoS item+ f iid k = do+ let container = actorContainer aid (binv b) iid+ item <- getsState $ getItemBody iid+ if isDestroyed item then+ case isExplosive coitem discoS item of+ Nothing -> execCmdAtomic $ DestroyItemA iid item k container+ Just cgroup -> do+ let ik = fromJust $ jkind discoS item+ execCmdAtomic $ DiscoverA (blid b) (bpos b) iid ik+ execCmdAtomic $ DestroyItemA iid item k container+ explodeItem aid b container cgroup+ else+ execCmdAtomic $ MoveItemA iid k container (CFloor (blid b) (bpos b)) mapActorItems_ f b +explodeItem :: (MonadAtomic m, MonadServer m)+ => ActorId -> Actor -> Container -> Text -> m ()+explodeItem aid b container cgroup = do+ Kind.COps{coitem} <- getsState scops+ flavour <- getsServer sflavour+ discoRev <- getsServer sdiscoRev+ Level{ldepth} <- getLevel $ blid b+ depth <- getsState sdepth+ let itemFreq = toFreq "shrapnel group" [(1, cgroup)]+ (item, n1, _) <- rndToAction+ $ newItem coitem flavour discoRev itemFreq ldepth depth+ iid <- registerItem item n1 container False+ let Point x y = bpos b+ projectN n = replicateM_ n $ do+ tpxy <- rndToAction $ do+ border <- randomR (1, 4)+ -- We pick a point at the border, not inside, to have a uniform+ -- distribution for the points the line goes through at each distance+ -- from the source. Otherwise, e.g., the points on cardinal+ -- and diagonal lines from the source would be more common.+ case border :: Int of+ 1 -> fmap (Point (x - 10)) $ randomR (y - 10, y + 10)+ 2 -> fmap (Point (x + 10)) $ randomR (y - 10, y + 10)+ 3 -> fmap (flip Point (y - 10)) $ randomR (x - 10, x + 10)+ 4 -> fmap (flip Point (y + 10)) $ randomR (x - 10, x + 10)+ _ -> assert `failure` border+ let eps = px tpxy + py tpxy+ mfail <- projectFail aid tpxy eps iid container True+ case mfail of+ Nothing -> return ()+ Just ProjectBlockTerrain -> return ()+ Just failMsg -> execFailure aid failMsg+ projectN n1+ bag2 <- getsState $ bbag . getActorBody aid+ let mn2 = EM.lookup iid bag2+ maybe skip projectN mn2 -- assume all shrapnels bounce off obstacles once+ bag3 <- getsState $ bbag . getActorBody aid+ let mn3 = EM.lookup iid bag3+ maybe skip (\k -> execCmdAtomic $ LoseItemA iid item k container) mn3+ -- | Advance the move time for the given actor. advanceTime :: MonadAtomic m => ActorId -> m () advanceTime aid = do b <- getsState $ getActorBody aid- -- Don't update move time, so move ASAP, so the projectile- -- corpse vanishes ASAP.- unless (bhp b < 0 && bproj b || maybe False null (bpath b)) $ do- let t = ticksPerMeter $ bspeed b- execCmdAtomic $ AgeActorA aid t+ let t = ticksPerMeter $ bspeed b+ execCmdAtomic $ AgeActorA aid t -- | Generate a monster, possibly. generateMonster :: (MonadAtomic m, MonadServer m) => LevelId -> m ()@@ -352,9 +429,15 @@ spawns = actorNotProjList f lid s depth <- getsState sdepth rc <- rndToAction $ monsterGenChance ldepth depth (length spawns)+ factionD <- getsState sfactionD when rc $ do time <- getsState $ getLocalTime lid- mfid <- pickFaction "spawn" (const True)+ let freq = toFreq "spawn"+ $ map (\(fid, fact) -> (playerSpawn $ gplayer fact, fid))+ $ EM.assocs factionD+ mfid <- if nullFreq freq then+ return Nothing+ else fmap Just $ rndToAction $ frequency freq case mfid of Nothing -> return () -- no faction spawns Just fid -> do@@ -373,9 +456,9 @@ as = actorList (const True) lid s isLit = Tile.isLit cotile distantAtLeast d p _ =- all (\b -> chessDist lxsize (bpos b) p > d) inhabitants+ all (\b -> chessDist (bpos b) p > d) inhabitants findPosTry 40 ltile- ( \p t -> Tile.hasFeature cotile F.Walkable t+ ( \p t -> Tile.isWalkable cotile t && unoccupied as p) [ \_ t -> not (isLit t) -- no such tiles on some maps , distantAtLeast factionDist@@ -394,7 +477,7 @@ -- thing, but immediately (and destroy the item). -- | Possibly regenerate HP for all actors on the current level. ----- We really want leader selection to be a purely UI distinction,+-- We really want leader picking to be a purely UI distinction, -- so all actors need to regenerate, not just the leaders. -- Actors on frozen levels don't regenerate. This prevents cheating -- via sending an actor to a safe level and letting him regenerate there.@@ -418,10 +501,54 @@ || bhp m <= 0 then Nothing else Just a- toRegen <-- getsState $ mapMaybe approve . actorNotProjAssocs (const True) lid+ toRegen <- getsState $ mapMaybe approve . actorNotProjAssocs (const True) lid mapM_ (\aid -> execCmdAtomic $ HealActorA aid 1) toRegen +leadLevelFlip :: (MonadAtomic m, MonadServer m) => m ()+leadLevelFlip = do+ Kind.COps{cotile} <- getsState scops+ let canFlip fact = playerAiLeader (gplayer fact)+ || isSpawnFact fact+ flipFaction fact | not $ canFlip fact = return ()+ flipFaction fact = do+ case gleader fact of+ Nothing -> return ()+ Just leader -> do+ body <- getsState $ getActorBody leader+ lvl2 <- getLevel $ blid body+ let leaderStuck = waitedLastTurn body+ t = lvl2 `at` bpos body+ -- Keep the leader: he is on stairs and not stuck+ -- and we don't want to clog stairs or get pushed to another level.+ unless (not leaderStuck && Tile.isStair cotile t) $ do+ actorD <- getsState sactorD+ let ourLvl (lid, lvl) =+ ( lid+ , EM.size (lfloor lvl)+ , actorNotProjAssocsLvl (== bfid body) lvl actorD )+ ours <- getsState $ map ourLvl . EM.assocs . sdungeon+ -- Non-humans, being born in the dungeon, have a rough idea of+ -- the number of items left on the level and will focus+ -- on levels they started exploring and that have few items+ -- left. This is to to explore them completely, leave them+ -- once and for all and concentrate forces on another level.+ -- In addition, sole stranded actors tend to become leaders+ -- so that they can join the main force ASAP.+ let freqList = [ (k, (lid, a))+ | (lid, itemN, (a, b) : rest) <- ours+ , bhp b > 0 -- drama levels skipped+ , not leaderStuck || lid /= blid body+ , let len = 1 + (min 10 $ length rest)+ k = 1000000 `div` (3 * itemN + len) ]+ unless (null freqList) $ do+ (lid, a) <- rndToAction $ frequency+ $ toFreq "leadLevel" freqList+ unless (lid == blid body) $+ execCmdAtomic+ $ LeadFactionA (bfid body) (Just leader) (Just a)+ factionD <- getsState sfactionD+ mapM_ flipFaction $ EM.elems factionD+ -- | Continue or exit or restart the game. endOrLoop :: (MonadAtomic m, MonadConnServer m) => m () -> m () -> m () endOrLoop updConn loopServer = do@@ -444,8 +571,8 @@ modifyServer $ \ser -> ser {sdebugNxt = (sdebugNxt ser) {sgameMode}} restartGame updConn loopServer _ | gameOver -> restartGame updConn loopServer- (_, []) -> loopServer -- continue current game- (_, _ : _) -> do+ ([], []) -> loopServer -- continue current game+ ([], _ : _) -> do -- Wipe out the quit flag for the savegame files. mapM_ (\(fid, fact) -> execCmdAtomic@@ -458,7 +585,7 @@ saveAndExit = do cops <- getsState scops -- Save client and server data.- saveBkpAll+ saveBkpAll True -- debugPrint "Server saves game before exit" -- Kill all clients, including those that did not take part -- in the current game.@@ -476,11 +603,14 @@ restartGame :: (MonadAtomic m, MonadConnServer m) => m () -> m () -> m () restartGame updConn loopServer = do+ tellGameClipPS cops <- getsState scops sdebugNxt <- getsServer sdebugNxt srandom <- getsServer srandom s <- gameReset cops sdebugNxt $ Just srandom- modifyServer $ \ser -> ser {sdebugNxt, sdebugSer = sdebugNxt}+ let debugBarRngs = sdebugNxt {sdungeonRng = Nothing, smainRng = Nothing}+ modifyServer $ \ser -> ser { sdebugNxt = debugBarRngs+ , sdebugSer = debugBarRngs } execCmdAtomic $ RestartServerA s updConn initPer
Game/LambdaHack/Server/ServerSem.hs view
@@ -9,6 +9,7 @@ -- TODO: document module Game.LambdaHack.Server.ServerSem where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM import Data.Key (mapWithKeyM_)@@ -17,12 +18,13 @@ import Data.Text (Text) import qualified NLP.Miniutter.English as MU -import Control.Exception.Assert.Sugar import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.Animation import Game.LambdaHack.Common.AtomicCmd import qualified Game.LambdaHack.Common.Color as Color+import Game.LambdaHack.Common.Effect import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Feature as F import Game.LambdaHack.Common.Item@@ -45,15 +47,18 @@ import Game.LambdaHack.Server.State execFailure :: (MonadAtomic m, MonadServer m)- => Actor -> FailureSer -> m ()-execFailure body failureSer = do+ => ActorId -> FailureSer -> m ()+execFailure aid failureSer = do -- Clients should rarely do that (only in case of invisible actors) -- so we report it, send a --more-- meeesage (if not AI), but do not crash -- (server should work OK with stupid clients, too).+ body <- getsState $ getActorBody aid let fid = bfid body msg = showFailureSer failureSer- debugPrint $ "execFailure:" <+> showT fid <+> ":" <+> msg- execSfxAtomic $ MsgFidD fid msg -- TODO: --more--+ debugPrint+ $ "execFailure:" <+> tshow fid <+> ":" <+> msg <> "\n" <> tshow body+ execSfxAtomic $ MsgFidD fid $ "Unexpected problem:" <+> msg <> "."+ -- TODO: --more--, but keep in history broadcastCmdAtomic :: MonadAtomic m => (FactionId -> CmdAtomic) -> m ()@@ -69,21 +74,16 @@ -- * MoveSer -checkAdjacent :: MonadActionRO m => Actor -> Actor -> m Bool-checkAdjacent sb tb = do- Level{lxsize} <- getLevel $ blid sb- return $ blid sb == blid tb && adjacent lxsize (bpos sb) (bpos tb)- -- TODO: let only some actors/items leave smell, e.g., a Smelly Hide Armour.--- | Add a smell trace for the actor to the level. For now, all and only--- actors from non-spawning factions leave smell.+-- | Add a smell trace for the actor to the level. For now, only heroes+-- leave smell. addSmell :: MonadAtomic m => ActorId -> m () addSmell aid = do- Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ cops@Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops b <- getsState $ getActorBody aid- spawn <- getsState $ isSpawnFaction (bfid b)+ fact <- getsState $ (EM.! bfid b) . sfactionD let canSmell = asmell $ okind $ bkind b- unless (bproj b || spawn || canSmell) $ do+ unless (bproj b || not (isHeroFact cops fact) || canSmell) $ do time <- getsState $ getLocalTime $ blid b lvl <- getLevel $ blid b let oldS = EM.lookup (bpos b) . lsmell $ lvl@@ -107,17 +107,18 @@ -- We start by checking actors at the the target position. tgt <- getsState $ posToActor tpos lid case tgt of- Just target -> -- visible or not+ Just ((target, tb), _) | not (bproj sb && bproj tb) -> -- visible or not -- Attacking does not require full access, adjacency is enough.+ -- Projectiles are too small to hit each other. meleeSer source target- Nothing+ _ | accessible cops lvl spos tpos -> do -- Movement requires full access. execCmdAtomic $ MoveActorA source spos tpos addSmell source | otherwise -> -- Client foolishly tries to move into blocked, boring tile.- execFailure sb MoveNothing+ execFailure source MoveNothing -- * MeleeSer @@ -126,17 +127,20 @@ -- For instance, an actor embedded in a wall can be attacked from -- an adjacent position. This function is analogous to projectGroupItem, -- but for melee and not using up the weapon.+-- No problem if there are many projectiles at the spot. We just+-- attack the one specified. meleeSer :: (MonadAtomic m, MonadServer m) => ActorId -> ActorId -> m () meleeSer source target = do- cops@Kind.COps{coitem=Kind.Ops{opick, okind}} <- getsState scops+ cops@Kind.COps{coitem=coitem@Kind.Ops{opick, okind}} <- getsState scops sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target- adj <- checkAdjacent sb tb- if not adj then execFailure sb MeleeDistant+ let adj = checkAdjacent sb tb+ if source == target then execFailure source MeleeSelf+ else if not adj then execFailure source MeleeDistant else do let sfid = bfid sb tfid = bfid tb- time <- getsState $ getLocalTime (blid tb)+ sfact <- getsState $ (EM.! sfid) . sfactionD itemAssocs <- getsState $ getActorItem source (miid, item) <- if bproj sb -- projectile@@ -146,15 +150,18 @@ else case strongestSword cops itemAssocs of Just (_, (iid, w)) -> return (Just iid, w) -- weapon combat Nothing -> do -- hand to hand combat- isSp <- getsState $ isSpawnFaction sfid- let h2hGroup | isSp = "monstrous"- | otherwise = "unarmed"+ let isHero = isHeroFact cops sfact+ h2hGroup | isHero = "unarmed"+ | otherwise = "monstrous" h2hKind <- rndToAction $ fmap (fromMaybe $ assert `failure` h2hGroup) $ opick h2hGroup (const True) flavour <- getsServer sflavour discoRev <- getsServer sdiscoRev let kind = okind h2hKind- effect = fmap maxDeep (ieffect kind)+ let kindEffect = case causeIEffects coitem h2hKind of+ [] -> NoEffect+ eff : _TODO -> eff+ effect = fmap maxDeep kindEffect return ( Nothing , buildItem flavour discoRev h2hKind kind effect ) let performHit block = do@@ -162,18 +169,17 @@ execSfxAtomic $ StrikeD source target item hitA -- Deduct a hitpoint for a pierce of a projectile. when (bproj sb) $ execCmdAtomic $ HealActorA source (-1)- -- Msgs inside itemEffectSem describe the target part.+ -- Msgs inside itemEffect describe the target part. itemEffect source target miid item -- Projectiles can't be blocked (though can be sidestepped). -- Incapacitated actors can't block.- if braced tb time && not (bproj sb) && bhp tb > 0+ if braced tb && not (bproj sb) && bhp tb > 0 then do blocked <- rndToAction $ chance $ 1%2 if blocked then execSfxAtomic $ StrikeD source target item MissBlockD else performHit True else performHit False- sfact <- getsState $ (EM.! sfid) . sfactionD -- The only way to start a war is to slap an enemy. Being hit by -- and hitting projectiles count as unintentional friendly fire. let friendlyFire = bproj sb || bproj tb@@ -189,20 +195,26 @@ cops <- getsState scops sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target- adj <- checkAdjacent sb tb- if not adj then execFailure sb DisplaceDistant+ let adj = checkAdjacent sb tb+ if not adj then execFailure source DisplaceDistant else do let lid = blid sb lvl <- getLevel lid let spos = bpos sb tpos = bpos tb+ -- Displacing requires full access. if accessible cops lvl spos tpos then do- -- Displacing requires full access.- execCmdAtomic $ DisplaceActorA source target- addSmell source+ tgts <- getsState $ posToActors tpos lid+ case tgts of+ [] -> assert `failure` (source, sb, target, tb)+ [_] -> do+ execCmdAtomic $ DisplaceActorA source target+ addSmell source+ addSmell target+ _ -> execFailure source DisplaceProjectiles else do -- Client foolishly tries to displace an actor without access.- execFailure sb DisplaceAccess+ execFailure source DisplaceAccess -- * AlterSer @@ -217,8 +229,7 @@ sb <- getsState $ getActorBody source let lid = blid sb spos = bpos sb- Level{lxsize} <- getLevel lid- if not $ adjacent lxsize spos tpos then execFailure sb AlterDistant+ if not $ adjacent spos tpos then execFailure source AlterDistant else do lvl <- getLevel lid let serverTile = lvl `at` tpos@@ -243,7 +254,7 @@ as <- getsState $ actorList (const True) lid if null groupsToAlter && serverTile == freshClientTile then -- Neither searching nor altering possible; silly client.- execFailure sb AlterNothing+ execFailure source AlterNothing else do if EM.null $ lvl `atI` tpos then if unoccupied as tpos then do@@ -254,86 +265,111 @@ mapM_ changeTo groupsToAlter -- Perform an effect, if any permitted. void $ triggerEffect source feats- else execFailure sb AlterBlockActor- else execFailure sb AlterBlockItem+ else execFailure source AlterBlockActor+ else execFailure source AlterBlockItem -- * WaitSer --- | Update the wait/block count. Uses local, per-level time,--- to remain correct even if the level is frozen for some global time turns.+-- | Do nothing.+--+-- Something is sometimes done in 'LoopAction.setBWait'. waitSer :: MonadAtomic m => ActorId -> m ()-waitSer aid = do- body <- getsState $ getActorBody aid- time <- getsState $ getLocalTime $ blid body- let fromWait = bwait body- toWait = timeAddFromSpeed body time- execCmdAtomic $ WaitActorA aid fromWait toWait+waitSer _ = return () -- * PickupSer -pickupSer :: MonadAtomic m- => ActorId -> ItemId -> Int -> InvChar -> m ()-pickupSer aid iid k l = assert (k > 0 `blame` "pick up no items"- `twith` (aid, iid, k, l)) $ do+pickupSer :: (MonadAtomic m, MonadServer m)+ => ActorId -> ItemId -> Int -> m ()+pickupSer aid iid k = assert (k > 0) $ do b <- getsState $ getActorBody aid- execCmdAtomic $ MoveItemA iid k (CFloor (blid b) (bpos b)) (CActor aid l)+ item <- getsState $ getItemBody iid+ case actorContainerB aid b iid item of+ Just c -> execCmdAtomic $ MoveItemA iid k (CFloor (blid b) (bpos b)) c+ Nothing -> execFailure aid PickupOverfull -- * DropSer -dropSer :: MonadAtomic m => ActorId -> ItemId -> m ()-dropSer aid iid = do+dropSer :: MonadAtomic m => ActorId -> ItemId -> Int -> m ()+dropSer aid iid k = assert (k > 0) $ do b <- getsState $ getActorBody aid- let k = 1- execCmdAtomic $ MoveItemA iid k (actorContainer aid (binv b) iid)- (CFloor (blid b) (bpos b))+ let c = actorContainer aid (binv b) iid+ execCmdAtomic $ MoveItemA iid k c (CFloor (blid b) (bpos b)) -- * ProjectSer projectSer :: (MonadAtomic m, MonadServer m) => ActorId -- ^ actor projecting the item (is on current lvl)- -> Point -- ^ target position of the projectile+ -> Point -- ^ target position of the projectile -> Int -- ^ digital line parameter -> ItemId -- ^ the item to be projected -> Container -- ^ whether the items comes from floor or inventory -> m ()-projectSer source tpos eps iid container = do- Kind.COps{cotile} <- getsState scops+projectSer source tpxy eps iid container = do+ mfail <- projectFail source tpxy eps iid container False+ maybe skip (execFailure source) mfail++projectFail :: (MonadAtomic m, MonadServer m)+ => ActorId -- ^ actor projecting the item (is on current lvl)+ -> Point -- ^ target position of the projectile+ -> Int -- ^ digital line parameter+ -> ItemId -- ^ the item to be projected+ -> Container -- ^ whether the items comes from floor or inventory+ -> Bool -- ^ whether the item is a shrapnel+ -> m (Maybe FailureSer)+projectFail source tpxy eps iid container isShrapnel = do+ Kind.COps{coactor=Kind.Ops{okind}, cotile} <- getsState scops sb <- getsState $ getActorBody source let lid = blid sb spos = bpos sb- fact <- getsState $ (EM.! bfid sb) . sfactionD- Level{lxsize, lysize} <- getLevel lid- foes <- getsState $ actorNotProjList (isAtWar fact) lid- if foesAdjacent lxsize lysize spos foes- then execFailure sb ProjectBlockFoes- else do- case bla lxsize lysize eps spos tpos of- Nothing -> execFailure sb ProjectAimOnself- Just [] -> assert `failure` "projecting from the edge of level"- `twith` (spos, tpos)- Just (pos : rest) -> do- as <- getsState $ actorList (const True) lid- lvl <- getLevel lid- let t = lvl `at` pos- if not $ Tile.hasFeature cotile F.Clear t- then execFailure sb ProjectBlockTerrain- else if unoccupied as pos- then projectBla source pos rest iid container- else execFailure sb ProjectBlockActor+ lvl@Level{lxsize, lysize} <- getLevel lid+ case bla lxsize lysize eps spos tpxy of+ Nothing -> return $ Just ProjectAimOnself+ Just [] -> assert `failure` "projecting from the edge of level"+ `twith` (spos, tpxy)+ Just (pos : rest) -> do+ let t = lvl `at` pos+ if not $ Tile.isClear cotile t+ then return $ Just ProjectBlockTerrain+ else do+ mab <- getsState $ posToActor pos lid+ if not $ maybe True (bproj . snd . fst) mab+ then+ if isShrapnel then do+ -- Hit the blocking actor.+ projectBla source spos (pos : rest) iid container+ return Nothing+ else return $ Just ProjectBlockActor+ else do+ blockedByFoes <-+ if isShrapnel then return False+ else do+ fact <- getsState $ (EM.! bfid sb) . sfactionD+ foes <- getsState $ actorNotProjList (isAtWar fact) lid+ return $! foesAdjacent lxsize lysize spos foes+ if blockedByFoes then+ return $ Just ProjectBlockFoes+ else if not (asight (okind $ bkind sb) || bproj sb)+ then return $ Just ProjectBlind+ else do+ if isShrapnel && eps `mod` 2 == 0 then+ -- Make the explosion a bit less regular.+ projectBla source spos (pos:rest) iid container+ else+ projectBla source pos rest iid container+ return Nothing projectBla :: (MonadAtomic m, MonadServer m) => ActorId -- ^ actor projecting the item (is on current lvl) -> Point -- ^ starting point of the projectile- -> [Point] -- ^ rest of the path of the projectile+ -> [Point] -- ^ rest of the trajectory of the projectile -> ItemId -- ^ the item to be projected -> Container -- ^ whether the items comes from floor or inventory -> m () projectBla source pos rest iid container = do sb <- getsState $ getActorBody source let lid = blid sb- -- A bit later than actor time, to prevent a move this turn.- time = btime sb `timeAdd` timeEpsilon- execSfxAtomic $ ProjectD source iid+ time = btime sb+ unless (bproj sb) $ execSfxAtomic $ ProjectD source iid projId <- addProjectile pos rest iid lid (bfid sb) time execCmdAtomic $ MoveItemA iid 1 container (CActor projId (InvChar 'a')) @@ -346,23 +382,25 @@ , coitem=coitem@Kind.Ops{okind=iokind} } <- getsState scops disco <- getsServer sdisco item <- getsState $ getItemBody iid- let ik = iokind (fromJust $ jkind disco item)- speed = speedFromWeight (iweight ik) (itoThrow ik)+ let lingerPercent = isLingering coitem disco item+ ik = iokind (fromJust $ jkind disco item)+ speed = speedFromWeight (jweight item) (itoThrow ik) range = rangeFromSpeed speed adj | range < 5 = "falling" | otherwise = "flying" -- Not much details about a fast flying object. (object1, object2) = partItem coitem EM.empty item name = makePhrase [MU.AW $ MU.Text adj, object1, object2]- dirPath = take range $ displacePath (bpos : rest)+ trajectoryLength = lingerPercent * range `div` 100+ dirTrajectory = take trajectoryLength $ pathToTrajectory (bpos : rest) kind = okind $ projectileKindId coactor m = actorTemplate (projectileKindId coactor) (asymbol kind) name- (acolor kind) speed 0 (Just dirPath)+ (acolor kind) speed 0 (Just dirTrajectory) bpos blid btime bfid True acounter <- getsServer sacounter modifyServer $ \ser -> ser {sacounter = succ acounter} execCmdAtomic $ CreateActorA acounter m [(iid, item)]- return acounter+ return $! acounter -- * ApplySer @@ -396,7 +434,7 @@ Just feat2 | Tile.hasFeature cotile feat2 serverTile -> [feat2] Just _ -> [] go <- triggerEffect aid feats- unless go $ execFailure sb TriggerNothing+ unless go $ execFailure aid TriggerNothing triggerEffect :: (MonadAtomic m, MonadServer m) => ActorId -> [F.Feature] -> m Bool@@ -414,39 +452,60 @@ goes <- mapM triggerFeat feats return $! or goes --- * SetPathSer+-- * SetTrajectorySer -setPathSer :: (MonadAtomic m, MonadServer m)- => ActorId -> [Vector] -> m ()-setPathSer aid path = do- when (length path <= 2) $ do- fromColor <- getsState $ bcolor . getActorBody aid- let toColor = Color.BrBlack- when (fromColor /= toColor) $- execCmdAtomic $ ColorActorA aid fromColor toColor- fromPath <- getsState $ bpath . getActorBody aid- case path of- [] -> execCmdAtomic $ PathActorA aid fromPath (Just [])- d : lv -> do- void $ moveSer aid d- execCmdAtomic $ PathActorA aid fromPath (Just lv)+setTrajectorySer :: (MonadAtomic m, MonadServer m) => ActorId -> m ()+setTrajectorySer aid = do+ cops <- getsState scops+ b@Actor{bpos, btrajectory, blid, bcolor} <- getsState $ getActorBody aid+ lvl <- getLevel blid+ let clearTrajectory =+ execCmdAtomic $ TrajectoryActorA aid btrajectory (Just [])+ case btrajectory of+ Just (d : lv) ->+ if not $ accessibleDir cops lvl bpos d+ then clearTrajectory+ else do+ when (length lv <= 1) $ do+ let toColor = Color.BrBlack+ when (bcolor /= toColor) $+ execCmdAtomic $ ColorActorA aid bcolor toColor+ moveSer aid d+ execCmdAtomic $ TrajectoryActorA aid btrajectory (Just lv)+ _ -> assert `failure` "null trajectory" `twith` (aid, b) -- * GameRestart -gameRestartSer :: (MonadAtomic m, MonadServer m) => ActorId -> Text -> m ()-gameRestartSer aid stInfo = do+-- TODO: implement a handshake and send hero names there,+-- so that they are available in the first game too,+-- not only in subsequent, restarted, games.+gameRestartSer :: (MonadAtomic m, MonadServer m)+ => ActorId -> Text -> Int -> [(Int, Text)] -> m ()+gameRestartSer aid stInfo d configHeroNames = do+ modifyServer $ \ser ->+ ser {sdebugNxt = (sdebugNxt ser) { sdifficultySer = d+ , sdebugCli = (sdebugCli (sdebugNxt ser))+ {sdifficultyCli = d}+ }} b <- getsState $ getActorBody aid let fid = bfid b oldSt <- getsState $ gquit . (EM.! fid) . sfactionD- modifyServer $ \ser -> ser {squit = True} -- do this at once+ modifyServer $ \ser ->+ ser { squit = True -- do this at once+ , sheroNames = EM.insert fid configHeroNames $ sheroNames ser } revealItems Nothing Nothing execCmdAtomic $ QuitFactionA fid (Just b) oldSt $ Just $ Status Restart (fromEnum $ blid b) stInfo -- * GameExit -gameExitSer :: (MonadAtomic m, MonadServer m) => ActorId -> m ()-gameExitSer aid = do+gameExitSer :: (MonadAtomic m, MonadServer m) => ActorId -> Int -> m ()+gameExitSer aid d = do+ modifyServer $ \ser ->+ ser {sdebugNxt = (sdebugNxt ser) { sdifficultySer = d+ , sdebugCli = (sdebugCli (sdebugNxt ser))+ {sdifficultyCli = d}+ }} b <- getsState $ getActorBody aid let fid = bfid b oldSt <- getsState $ gquit . (EM.! fid) . sfactionD
Game/LambdaHack/Server/StartAction.hs view
@@ -1,8 +1,9 @@ -- | Operations for starting and restarting the game. module Game.LambdaHack.Server.StartAction- ( applyDebug, gameReset, reinitGame, initPer+ ( applyDebug, gameReset, reinitGame, saveBkpAll, initPer ) where +import Control.Exception.Assert.Sugar import Control.Monad import qualified Control.Monad.State as St import qualified Data.Char as Char@@ -17,7 +18,6 @@ import Data.Tuple (swap) import qualified System.Random as R -import Control.Exception.Assert.Sugar import Game.LambdaHack.Common.Action import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.AtomicCmd@@ -25,21 +25,21 @@ import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Feature as F import Game.LambdaHack.Common.Flavour+import qualified Game.LambdaHack.Common.HighScore as HighScore import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Point-import Game.LambdaHack.Common.PointXY import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Server.Action hiding (sendUpdateAI, sendUpdateUI)-import Game.LambdaHack.Server.Config import qualified Game.LambdaHack.Server.DungeonGen as DungeonGen import Game.LambdaHack.Server.EffectSem import Game.LambdaHack.Server.Fov@@ -58,7 +58,7 @@ , sstopAfter , sdbgMsgSer , snewGameSer- , sdumpConfig+ , sdumpInitRngs , sdebugCli }} initPer :: MonadServer m => m ()@@ -68,9 +68,9 @@ pers <- getsState $ dungeonPerception cops (fromMaybe (Digital 12) fovMode) modifyServer $ \ser1 -> ser1 {sper = pers} -reinitGame :: (MonadAtomic m, MonadServer m) => m ()+reinitGame :: (MonadAtomic m, MonadConnServer m) => m () reinitGame = do- Kind.COps{ coitem=Kind.Ops{okind}, corule } <- getsState scops+ Kind.COps{coitem=Kind.Ops{okind}, corule} <- getsState scops pers <- getsServer sper knowMap <- getsServer $ sknowMap . sdebugSer -- This state is quite small, fit for transmition to the client.@@ -89,7 +89,26 @@ broadcastCmdAtomic $ \fid -> RestartA fid sdisco (pers EM.! fid) defLoc sdebugCli modeName populateDungeon+ saveBkpAll False +-- TODO: This can be improved by adding a timeout+-- and by asking clients to prepare+-- a save (in this way checking they have permissions, enough space, etc.)+-- and when all report back, asking them to commit the save.+-- | Save game on server and all clients. Clients are pinged first,+-- which greatly reduced the chance of saves being out of sync.+saveBkpAll :: (MonadAtomic m, MonadServer m, MonadConnServer m) => Bool -> m ()+saveBkpAll unconditional = do+ factionD <- getsState sfactionD+ let ping fid _ = do+ sendPingAI fid+ when (playerUI $ gplayer $ factionD EM.! fid) $ sendPingUI fid+ mapWithKeyM_ ping factionD+ bench <- getsServer $ sbenchmark . sdebugSer+ when (unconditional || not bench) $ do+ execCmdAtomic SaveBkpA+ saveServer+ mapFromInvFuns :: (Bounded a, Enum a, Ord b) => [a -> b] -> M.Map b a mapFromInvFuns = let fromFun f m1 =@@ -117,7 +136,7 @@ let gdipl = EM.empty -- fixed below gquit = Nothing gleader = Nothing- return Faction{..}+ return $! Faction{..} lUI <- mapM rawCreate $ filter playerUI $ playersList players lnoUI <- mapM rawCreate $ filter (not . playerUI) $ playersList players let lFs = reverse (zip [toEnum (-1), toEnum (-2)..] lnoUI) -- sorted@@ -142,18 +161,21 @@ -- War overrides alliance, so 'warFs' second. allianceFs = mkDipl Alliance rawFs (swapIx (playersAlly players)) warFs = mkDipl War allianceFs (swapIx (playersEnemy players))- return warFs+ return $! warFs gameReset :: MonadServer m => Kind.COps -> DebugModeSer -> Maybe R.StdGen -> m State-gameReset cops@Kind.COps{coitem, comode=Kind.Ops{opick, okind}, corule}+gameReset cops@Kind.COps{coitem, comode=Kind.Ops{opick, okind}} sdebug mrandom = do- -- Rules config reloaded at each new game start.- -- Taking the original config from config file, to reroll RNG, if needed- -- (the current config file has the RNG rolled for the previous game).- (sconfig, dungeonSeed, srandom) <- mkConfigRules corule mrandom- scoreTable <- restoreScore sconfig+ dungeonSeed <- getSetGen $ sdungeonRng sdebug `mplus` mrandom+ srandom <- getSetGen $ smainRng sdebug `mplus` mrandom+ scoreTable <- if sbenchmark sdebug then+ return HighScore.empty+ else+ restoreScore cops sstart <- getsServer sstart -- copy over from previous game+ sallTime <- getsServer sallTime -- copy over from previous game+ sheroNames <- getsServer sheroNames -- copy over from previous game let smode = sgameMode sdebug rnd :: Rnd (FactionDict, FlavourMap, Discovery, DiscoRev, DungeonGen.FreshDungeon)@@ -169,10 +191,13 @@ let (faction, sflavour, sdisco, sdiscoRev, DungeonGen.FreshDungeon{..}) = St.evalState rnd dungeonSeed defState = defStateGlobal freshDungeon freshDepth faction cops scoreTable- defSer = emptyStateServer- {sdisco, sdiscoRev, sflavour, srandom, sconfig, sstart}+ defSer = emptyStateServer { sstart, sallTime, sheroNames, srandom+ , srngs = RNGs (Just dungeonSeed)+ (Just srandom) } putServer defSer- return defState+ when (sbenchmark sdebug) resetGameStart+ modifyServer $ \ser -> ser {sdisco, sdiscoRev, sflavour}+ return $! defState -- Spawn initial actors. Clients should notice this, to set their leaders. populateDungeon :: (MonadAtomic m, MonadServer m) => m ()@@ -192,7 +217,7 @@ dungeon <- getsState sdungeon mapWithKeyM_ initialItems dungeon factionD <- getsState sfactionD- Config{configHeroNames} <- getsServer sconfig+ sheroNames <- getsServer sheroNames let (minD, maxD) = case (EM.minViewWithKey dungeon, EM.maxViewWithKey dungeon) of (Just ((s, _), _), Just ((e, _), _)) -> (s, e)@@ -218,10 +243,11 @@ psFree <- getsState $ nearbyFreePoints cotile validTile ppos lid let ps = take (playerInitial $ gplayer fact) $ zip [0..] psFree forM_ ps $ \ (n, p) ->- if isSpawnFact cops fact+ if not $ isHeroFact cops fact then spawnMonsters [p] lid ntime side else do- aid <- addHero side p lid configHeroNames (Just n) ntime+ let hNames = fromMaybe [] $ EM.lookup side sheroNames+ aid <- addHero side p lid hNames (Just n) ntime mleader <- getsState $ gleader . (EM.! side) . sfactionD -- just changed when (isNothing mleader) $@@ -233,7 +259,7 @@ findEntryPoss :: Kind.COps -> Level -> Int -> Rnd [Point] findEntryPoss Kind.COps{cotile} Level{ltile, lxsize, lysize, lstair} k = do let factionDist = max lxsize lysize - 5- dist poss cmin l _ = all (\pos -> chessDist lxsize l pos > cmin) poss+ dist poss cmin l _ = all (\pos -> chessDist l pos > cmin) poss tryFind _ 0 = return [] tryFind ps n = do np <- findPosTry 1000 ltile -- try really hard, for skirmish fairness@@ -245,9 +271,9 @@ , dist ps $ factionDist `div` 16 ] nps <- tryFind (np : ps) (n - 1)- return $ np : nps+ return $! np : nps stairPoss = fst lstair ++ snd lstair- middlePos = toPoint lxsize $ PointXY (lxsize `div` 2, lysize `div` 2)+ middlePos = Point (lxsize `div` 2) (lysize `div` 2) assert (k > 0 && factionDist > 0) skip case k of 1 -> tryFind stairPoss k
Game/LambdaHack/Server/State.hs view
@@ -2,11 +2,13 @@ module Game.LambdaHack.Server.State ( StateServer(..), emptyStateServer , DebugModeSer(..), defDebugModeSer+ , RNGs(..) ) where import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.HashMap.Strict as HM+import Data.List import Data.Text (Text) import qualified System.Random as R import System.Time@@ -14,28 +16,33 @@ import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.Animation import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Perception-import Game.LambdaHack.Server.Config-import Game.LambdaHack.Server.Fov+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.RuleKind -- | Global, server state. data StateServer = StateServer- { sdisco :: !Discovery -- ^ full item discoveries data- , sdiscoRev :: !DiscoRev -- ^ reverse disco map, used for item creation- , sitemRev :: !ItemRev -- ^ reverse id map, used for item creation- , sflavour :: !FlavourMap -- ^ association of flavour to items- , sacounter :: !ActorId -- ^ stores next actor index- , sicounter :: !ItemId -- ^ stores next item index- , sundo :: ![Atomic] -- ^ atomic commands performed to date- , sper :: !Pers -- ^ perception of all factions- , srandom :: !R.StdGen -- ^ current random generator- , sconfig :: Config -- ^ this game's config (including initial RNG)- , squit :: !Bool -- ^ exit the game loop- , sbkpSave :: !Bool -- ^ make backup savefile now- , sstart :: !ClockTime -- ^ this session start time- , sdebugSer :: !DebugModeSer -- ^ current debugging mode- , sdebugNxt :: !DebugModeSer -- ^ debugging mode for the next game+ { sdisco :: !Discovery -- ^ full item discoveries data+ , sdiscoRev :: !DiscoRev -- ^ reverse disco map, used for item creation+ , sitemRev :: !ItemRev -- ^ reverse id map, used for item creation+ , sflavour :: !FlavourMap -- ^ association of flavour to items+ , sacounter :: !ActorId -- ^ stores next actor index+ , sicounter :: !ItemId -- ^ stores next item index+ , sundo :: ![Atomic] -- ^ atomic commands performed to date+ , sper :: !Pers -- ^ perception of all factions+ , srandom :: !R.StdGen -- ^ current random generator+ , srngs :: !RNGs -- ^ initial random generators+ , squit :: !Bool -- ^ exit the game loop+ , sbkpSave :: !Bool -- ^ make backup savefile now+ , sstart :: !ClockTime -- ^ this session start time+ , sgstart :: !ClockTime -- ^ this game start time+ , sallTime :: !Time -- ^ clips since the start of the session+ , sheroNames :: !(EM.EnumMap FactionId [(Int, Text)])+ -- ^ hero names sent by clients+ , sdebugSer :: !DebugModeSer -- ^ current debugging mode+ , sdebugNxt :: !DebugModeSer -- ^ debugging mode for the next game } deriving (Show) @@ -48,15 +55,32 @@ , sallClear :: !Bool , sgameMode :: !Text , sstopAfter :: !(Maybe Int)+ , sbenchmark :: !Bool+ , sdungeonRng :: !(Maybe R.StdGen)+ , smainRng :: !(Maybe R.StdGen) , sfovMode :: !(Maybe FovMode) , snewGameSer :: !Bool- , sdumpConfig :: !Bool+ , sdifficultySer :: !Int+ , sdumpInitRngs :: !Bool , ssavePrefixSer :: !(Maybe String) , sdbgMsgSer :: !Bool , sdebugCli :: !DebugModeCli } deriving Show +data RNGs = RNGs+ { dungeonRandomGenerator :: !(Maybe R.StdGen)+ , startingRandomGenerator :: !(Maybe R.StdGen)+ }++instance Show RNGs where+ show RNGs{..} =+ let args = [ maybe "" (\gen -> "--setDungeonRng \"" ++ show gen ++ "\"")+ dungeonRandomGenerator+ , maybe "" (\gen -> "--setMainRng \"" ++ show gen ++ "\"")+ startingRandomGenerator ]+ in intercalate " " args+ -- | Initial, empty game server state. emptyStateServer :: StateServer emptyStateServer =@@ -70,10 +94,14 @@ , sundo = [] , sper = EM.empty , srandom = R.mkStdGen 42- , sconfig = undefined+ , srngs = RNGs { dungeonRandomGenerator = Nothing+ , startingRandomGenerator = Nothing } , squit = False , sbkpSave = False , sstart = TOD 0 0+ , sgstart = TOD 0 0+ , sallTime = timeZero+ , sheroNames = EM.empty , sdebugSer = defDebugModeSer , sdebugNxt = defDebugModeSer }@@ -86,9 +114,13 @@ , sallClear = False , sgameMode = "campaign" , sstopAfter = Nothing+ , sbenchmark = False+ , sdungeonRng = Nothing+ , smainRng = Nothing , sfovMode = Nothing , snewGameSer = False- , sdumpConfig = False+ , sdifficultySer = 0+ , sdumpInitRngs = False , ssavePrefixSer = Nothing , sdbgMsgSer = False , sdebugCli = defDebugModeCli@@ -104,7 +136,8 @@ put sicounter put sundo put (show srandom)- put sconfig+ put srngs+ put sheroNames put sdebugSer get = do sdisco <- get@@ -115,15 +148,18 @@ sicounter <- get sundo <- get g <- get- sconfig <- get+ srngs <- get+ sheroNames <- get sdebugSer <- get let srandom = read g sper = EM.empty squit = False sbkpSave = False sstart = TOD 0 0- sdebugNxt = defDebugModeSer- return StateServer{..}+ sgstart = TOD 0 0+ sallTime = timeZero+ sdebugNxt = defDebugModeSer -- TODO: here difficulty level, etc. from the last session is wiped out+ return $! StateServer{..} instance Binary DebugModeSer where put DebugModeSer{..} = do@@ -133,6 +169,7 @@ put sniffOut put sallClear put sgameMode+ put sdifficultySer put sfovMode put ssavePrefixSer put sdbgMsgSer@@ -144,11 +181,26 @@ sniffOut <- get sallClear <- get sgameMode <- get+ sdifficultySer <- get sfovMode <- get ssavePrefixSer <- get sdbgMsgSer <- get sdebugCli <- get let sstopAfter = Nothing+ sbenchmark = False+ sdungeonRng = Nothing+ smainRng = Nothing snewGameSer = False- sdumpConfig = False- return DebugModeSer{..}+ sdumpInitRngs = False+ return $! DebugModeSer{..}++instance Binary RNGs where+ put RNGs{..} = do+ put (show dungeonRandomGenerator)+ put (show startingRandomGenerator)+ get = do+ dg <- get+ sg <- get+ let dungeonRandomGenerator = read dg+ startingRandomGenerator = read sg+ return $! RNGs{..}
Game/LambdaHack/Utils/File.hs view
@@ -1,20 +1,32 @@ -- | Saving/loading with serialization and compression. module Game.LambdaHack.Utils.File- ( encodeEOF, strictDecodeEOF, tryCreateDir, tryCopyDataFiles+ ( encodeEOF, strictDecodeEOF, tryCreateDir, tryCopyDataFiles, appDataDir ) where import qualified Codec.Compression.Zlib as Z+import qualified Control.Exception as Ex import Control.Monad import Data.Binary import qualified Data.ByteString.Lazy as LBS+import qualified Data.Char as Char import System.Directory+import System.Environment import System.FilePath import System.IO -- | Serialize, compress and save data. -- Note that LBS.writeFile opens the file in binary mode. encodeData :: Binary a => FilePath -> a -> IO ()-encodeData f = LBS.writeFile f . Z.compress . encode+encodeData f a = do+ let tmpPath = f <.> "tmp"+ Ex.bracketOnError+ (openBinaryFile tmpPath WriteMode)+ (\h -> hClose h >> removeFile tmpPath)+ (\h -> do+ LBS.hPut h . Z.compress . encode $ a+ hClose h+ renameFile tmpPath f+ ) -- | Serialize, compress and save data with an EOF marker. -- The @OK@ is used as an EOF marker to ensure any apparent problems with@@ -41,7 +53,7 @@ strictDecodeEOF f = do (a, n) <- strictDecodeData f if n == ("OK" :: String)- then return a+ then return $! a else error $ "Fatal error: corrupted file " ++ f -- | Try to create a directory, if it doesn't exist. Terminate the program@@ -56,11 +68,19 @@ -> (FilePath -> IO FilePath) -> [(FilePath, FilePath)] -> IO ()-tryCopyDataFiles configAppDataDir pathsDataFile files =+tryCopyDataFiles dataDir pathsDataFile files = let cpFile (fin, fout) = do- pathsDataIn <- pathsDataFile $ takeFileName fin+ pathsDataIn <- pathsDataFile fin bIn <- doesFileExist pathsDataIn- let pathsDataOut = configAppDataDir </> fout+ let pathsDataOut = dataDir </> fout bOut <- doesFileExist pathsDataOut when (not bOut && bIn) $ copyFile pathsDataIn pathsDataOut in mapM_ cpFile files++-- | 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 = takeWhile Char.isAlphaNum progName+ getAppUserDataDirectory name
Game/LambdaHack/Utils/Frequency.hs view
@@ -13,6 +13,7 @@ import Control.Applicative import Control.Arrow (first, second)+import Control.Exception.Assert.Sugar import Control.Monad import Data.Binary import Data.Foldable (Foldable)@@ -20,7 +21,6 @@ import Data.Traversable (Traversable) import qualified System.Random as R -import Control.Exception.Assert.Sugar import Game.LambdaHack.Common.Msg -- TODO: do not expose runFrequency@@ -32,6 +32,7 @@ deriving (Show, Eq, Foldable, Traversable) instance Monad Frequency where+ {-# INLINE return #-} return x = Frequency "return" [(1, x)] Frequency name xs >>= f = Frequency ("bind (" <> name <> ")")@@ -42,8 +43,8 @@ fmap f (Frequency name xs) = Frequency name (map (second f) xs) instance Applicative Frequency where- pure = return- (<*>) = ap+ pure = return+ (<*>) = ap instance MonadPlus Frequency where mplus (Frequency xname xs) (Frequency yname ys) =@@ -56,8 +57,8 @@ mzero = Frequency "[]" [] instance Alternative Frequency where- (<|>) = mplus- empty = mzero+ (<|>) = mplus+ empty = mzero -- | Uniform discrete frequency distribution. uniformFreq :: Text -> [a] -> Frequency a@@ -116,4 +117,4 @@ get = do nameFrequency <- get runFrequency <- get- return Frequency{..}+ return $! Frequency{..}
+ GameDefinition/Content/ActorKind.hs view
@@ -0,0 +1,88 @@+-- | Monsters and heroes for LambdaHack.+module Content.ActorKind ( cdefs ) where++import Game.LambdaHack.Common.Ability+import Game.LambdaHack.Common.Color+import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.ActorKind++cdefs :: ContentDef ActorKind+cdefs = ContentDef+ { getSymbol = asymbol+ , getName = aname+ , getFreq = afreq+ , validate = validateActorKind+ , content =+ [hero, projectile, eye, fastEye, nose]+ }+hero, projectile, eye, fastEye, nose :: ActorKind++hero = ActorKind+ { asymbol = '@'+ , aname = "hero"+ , afreq = [("hero", 1)]+ , acolor = BrWhite -- modified if many hero factions+ , ahp = rollDice 50 1+ , aspeed = toSpeed 2+ , asight = True+ , asmell = False+ , aiq = 15 -- higher that that leads to looping movement+ , aregen = 500+ , acanDo = [minBound..maxBound]+ }++projectile = ActorKind -- includes homing missiles+ { asymbol = '*'+ , aname = "projectile"+ , afreq = [("projectile", 1)] -- Does not appear randomly in the dungeon.+ , acolor = BrWhite+ , ahp = rollDice 0 0+ , aspeed = toSpeed 0+ , asight = False+ , asmell = False+ , aiq = 0+ , aregen = maxBound+ , acanDo = [Track]+ }++eye = ActorKind+ { asymbol = 'e'+ , aname = "reducible eye"+ , afreq = [("monster", 60), ("horror", 60)]+ , acolor = BrRed+ , ahp = rollDice 7 4+ , aspeed = toSpeed 2+ , asight = True+ , asmell = False+ , aiq = 8+ , aregen = 100+ , acanDo = [minBound..maxBound]+ }+fastEye = ActorKind+ { asymbol = 'e'+ , aname = "super-fast eye"+ , afreq = [("monster", 15), ("horror", 15)]+ , acolor = BrBlue+ , ahp = rollDice 1 6+ , aspeed = toSpeed 4+ , asight = True+ , asmell = False+ , aiq = 12+ , aregen = 10 -- Regenerates fast (at max HP most of the time!).+ , acanDo = [minBound..maxBound]+ }+nose = ActorKind+ { asymbol = 'n'+ , aname = "point-free nose"+ , afreq = [("monster", 20), ("horror", 20)]+ , acolor = Green+ , ahp = rollDice 17 2+ , aspeed = toSpeed 1.8+ , asight = False+ , asmell = True+ , aiq = 0+ , aregen = 100+ , acanDo = [minBound..maxBound]+ }
+ GameDefinition/Content/CaveKind.hs view
@@ -0,0 +1,123 @@+-- | Cave layouts for LambdaHack.+module Content.CaveKind ( cdefs ) where++import Data.Ratio++import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Random as Random+import Game.LambdaHack.Content.CaveKind++cdefs :: ContentDef CaveKind+cdefs = ContentDef+ { getSymbol = csymbol+ , getName = cname+ , getFreq = cfreq+ , validate = validateCaveKind+ , content =+ [rogue, arena, empty, noise, combat, battle]+ }+rogue, arena, empty, noise, combat, battle :: CaveKind++rogue = CaveKind+ { csymbol = '$'+ , cname = "A maze of twisty passages"+ , cfreq = [("dng", 100), ("caveRogue", 1)]+ , cxsize = fst normalLevelBound + 1+ , cysize = snd normalLevelBound + 1+ , cgrid = rollDiceXY [(3, 2)] [(1, 2), (2, 1)]+ , cminPlaceSize = rollDiceXY [(2, 2), (2, 1)] [(4, 1)]+ , cmaxPlaceSize = rollDiceXY [(fst normalLevelBound, 1)]+ [(snd normalLevelBound, 1)]+ , cdarkChance = rollDeep (1, 54) (0, 0)+ , cnightChance = intToDeep 100+ , cauxConnects = 1%3+ , cmaxVoid = 1%6+ , cminStairDist = 30+ , cdoorChance = 1%2+ , copenChance = 1%10+ , chidden = 8+ , citemNum = rollDice 7 2+ , citemFreq = [(70, "useful"), (30, "treasure")]+ , cdefTile = "fillerWall"+ , cdarkCorTile = "floorCorridorDark"+ , clitCorTile = "floorCorridorLit"+ , cfillerTile = "fillerWall"+ , couterFenceTile = "basic outer fence"+ , clegendDarkTile = "legendDark"+ , clegendLitTile = "legendLit"+ }+arena = rogue+ { csymbol = 'A'+ , cname = "Underground city"+ , cfreq = [("dng", 30), ("caveArena", 1)]+ , cgrid = rollDiceXY [(2, 2)] [(2, 2)]+ , cminPlaceSize = rollDiceXY [(2, 2), (3, 1)] [(4, 1)]+ , cdarkChance = rollDeep (1, 80) (1, 60)+ , cnightChance = intToDeep 0+ , cmaxVoid = 1%3+ , chidden = 1000+ , citemNum = rollDice 5 2 -- few rooms+ , cdefTile = "arenaSet"+ , cdarkCorTile = "trailLit" -- let paths around rooms be lit+ , clitCorTile = "trailLit"+ }+empty = rogue+ { csymbol = '.'+ , cname = "Tall cavern"+ , cfreq = [("dng", 20), ("caveEmpty", 1)]+ , cgrid = rollDiceXY [(1, 2), (1, 1)] [(1, 1)]+ , cminPlaceSize = rollDiceXY [(10, 1)] [(10, 1)]+ , cmaxPlaceSize = rollDiceXY [(fst normalLevelBound * 3 `div` 5, 1)]+ [(snd normalLevelBound * 3 `div` 5, 1)]+ , cdarkChance = rollDeep (1, 80) (1, 80)+ , cnightChance = intToDeep 0+ , cauxConnects = 1+ , cmaxVoid = 1%2+ , cminStairDist = 50+ , chidden = 1000+ , citemNum = rollDice 8 2 -- whole floor strewn with treasure+ , cdefTile = "emptySet"+ , cdarkCorTile = "trailLit" -- let paths around rooms be lit+ , clitCorTile = "floorArenaLit"+ }+noise = rogue+ { csymbol = '!'+ , cname = "Glittering cave"+ , cfreq = [("dng", 20), ("caveNoise", 1)]+ , cgrid = rollDiceXY [(2, 2)] [(1, 2), (1, 1)]+ , cminPlaceSize = rollDiceXY [(3, 2), (2, 1)] [(5, 1)]+ , cdarkChance = rollDeep (1, 80) (1, 40)+ , cnightChance = rollDeep (1, 40) (1, 40)+ , cmaxVoid = 0+ , chidden = 1000+ , citemNum = rollDice 4 2 -- fewer rooms+ , cdefTile = "noiseSet"+ , cdarkCorTile = "trailLit" -- let trails give off light+ , clitCorTile = "trailLit"+ }+combat = rogue+ { csymbol = 'C'+ , cname = "Combat arena"+ , cfreq = [("caveCombat", 1)]+ , cgrid = rollDiceXY [(2, 2), (3, 1)] [(1, 2), (2, 1)]+ , cminPlaceSize = rollDiceXY [(3, 1)] [(3, 1)]+ , cmaxPlaceSize = rollDiceXY [(5, 1)] [(5, 1)]+ , cdarkChance = intToDeep 100+ , cnightChance = rollDeep (1, 67) (0, 0)+ , chidden = 1000+ , cauxConnects = 0+ , cdoorChance = 1+ , copenChance = 0+ , citemNum = rollDice 12 2+ , citemFreq = [(100, "useful")]+ , cdefTile = "combatSet"+ , cdarkCorTile = "trailLit" -- let trails give off light+ , clitCorTile = "floorArenaLit"+ }+battle = combat -- TODO: actors can get stuck forever among trees+ { csymbol = 'B'+ , cname = "Battle arena"+ , cfreq = [("caveBattle", 1)]+ , cdefTile = "battleSet"+ }
+ GameDefinition/Content/FactionKind.hs view
@@ -0,0 +1,55 @@+-- | The type of kinds of game factions (heroes, enemies, NPCs, etc.)+-- for LambdaHack.+module Content.FactionKind ( cdefs ) where++import Game.LambdaHack.Common.Ability+import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Content.FactionKind++cdefs :: ContentDef FactionKind+cdefs = ContentDef+ { getSymbol = fsymbol+ , getName = fname+ , getFreq = ffreq+ , validate = validateFactionKind+ , content =+ [hero, monster, horror]+ }+hero, monster, horror :: FactionKind++hero = FactionKind+ { fsymbol = '@'+ , fname = "hero"+ , ffreq = [("hero", 1)]+ , fAbilityLeader = allAbilities+ , fAbilityOther = meleeAdjacent+ }++monster = FactionKind+ { fsymbol = 'm'+ , fname = "monster"+ , ffreq = [("monster", 1), ("summon", 50)]+ , fAbilityLeader = allAbilities+ , fAbilityOther = allAbilities+ }++horror = FactionKind+ { fsymbol = 'h'+ , fname = "horror"+ , ffreq = [("horror", 1), ("summon", 50)]+ , fAbilityLeader = allAbilities+ , fAbilityOther = allAbilities+ }+++_noAbility, _onlyFollowTrack, meleeAdjacent, _meleeAndRanged, allAbilities :: [Ability]++_noAbility = [] -- not even projectiles will fly++_onlyFollowTrack = [Track] -- projectiles enabled++meleeAdjacent = [Track, Melee]++_meleeAndRanged = [Track, Melee, Ranged] -- melee and reaction fire++allAbilities = [minBound..maxBound]
+ GameDefinition/Content/ItemKind.hs view
@@ -0,0 +1,267 @@+-- | Weapons and treasure for LambdaHack.+module Content.ItemKind ( cdefs ) where++import Game.LambdaHack.Common.Color+import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Common.Effect+import Game.LambdaHack.Common.Flavour+import Game.LambdaHack.Common.ItemFeature+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Content.ItemKind++cdefs :: ContentDef ItemKind+cdefs = ContentDef+ { getSymbol = isymbol+ , getName = iname+ , getFreq = ifreq+ , validate = validateItemKind+ , content =+ [amulet, dart, gem1, gem2, gem3, currency, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand1, wand2, fist, foot, tentacle, fragrance, mist_healing, mist_wounding, glass_piece, smoke]+ }+amulet, dart, gem1, gem2, gem3, currency, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand1, wand2, fist, foot, tentacle, fragrance, mist_healing, mist_wounding, glass_piece, smoke :: ItemKind++gem, potion, scroll, wand :: ItemKind -- generic templates++-- castDeep (aDb, xDy) = castDice aDb + (lvl - 1) * castDice xDy / (depth - 1)++amulet = ItemKind+ { isymbol = '"'+ , iname = "amulet"+ , ifreq = [("useful", 6)]+ , iflavour = zipFancy [BrGreen]+ , icount = intToDeep 1+ , iverbApply = "tear down"+ , iverbProject = "cast"+ , iweight = 30+ , itoThrow = -50 -- not dense enough+ , ifeature = [Cause $ Regeneration (rollDeep (2, 3) (1, 10))]+ }+dart = ItemKind+ { isymbol = '|'+ , iname = "dart"+ , ifreq = [("useful", 20)]+ , iflavour = zipPlain [Cyan]+ , icount = rollDeep (3, 3) (0, 0)+ , iverbApply = "snap"+ , iverbProject = "hurl"+ , iweight = 50+ , itoThrow = 0 -- a cheap dart+ , ifeature = [Cause $ Hurt (rollDice 1 2) (rollDeep (1, 2) (1, 2))]+ }+gem = ItemKind+ { isymbol = '*'+ , iname = "gem"+ , ifreq = [("treasure", 20)] -- x3, but rare on shallow levels+ , iflavour = zipPlain brightCol -- natural, so not fancy+ , icount = intToDeep 0+ , iverbApply = "crush"+ , iverbProject = "toss"+ , iweight = 50+ , itoThrow = 0+ , ifeature = []+ }+gem1 = gem+ { icount = rollDeep (0, 0) (1, 1) -- appears on max depth+ }+gem2 = gem+ { icount = rollDeep (0, 0) (1, 2) -- appears halfway+ }+gem3 = gem+ { icount = rollDeep (0, 0) (1, 3) -- appears early+ }+currency = ItemKind+ { isymbol = '$'+ , iname = "gold piece"+ , ifreq = [("treasure", 20), ("currency", 1)]+ , iflavour = zipPlain [BrYellow]+ , icount = rollDeep (0, 0) (10, 10) -- appears on lvl 2+ , iverbApply = "grind"+ , iverbProject = "toss"+ , iweight = 31+ , itoThrow = 0+ , ifeature = []+ }+harpoon = ItemKind+ { isymbol = '|'+ , iname = "harpoon"+ , ifreq = [("useful", 25)]+ , iflavour = zipPlain [Brown]+ , icount = rollDeep (0, 0) (2, 2)+ , iverbApply = "break up"+ , iverbProject = "hurl"+ , iweight = 4000+ , itoThrow = 0 -- cheap but deadly+ , ifeature = [Cause $ Hurt (rollDice 2 2) (rollDeep (1, 2) (2, 2))]+ }+potion = ItemKind+ { isymbol = '!'+ , iname = "potion"+ , ifreq = [("useful", 15)]+ , iflavour = zipFancy stdCol+ , icount = intToDeep 1+ , iverbApply = "gulp down"+ , iverbProject = "lob"+ , iweight = 200+ , itoThrow = -50 -- oily, bad grip+ , ifeature = []+ }+potion1 = potion+ { ifreq = [("useful", 5)]+ , ifeature = [Cause ApplyPerfume, Explode "fragrance"]+ }+potion2 = potion+ { ifeature = [Cause $ Heal 5, Explode "mist healing"]+ }+potion3 = potion+ { ifreq = [("useful", 5)]+ , ifeature = [Cause $ Heal (-5), Explode "mist wounding"]+ }+ring = ItemKind+ { isymbol = '='+ , iname = "ring"+ , ifreq = [] -- [("useful", 10)] -- TODO: make it useful+ , iflavour = zipPlain [White]+ , icount = intToDeep 1+ , iverbApply = "squeeze down"+ , iverbProject = "toss"+ , iweight = 15+ , itoThrow = 0+ , ifeature = [Cause $ Searching (rollDeep (1, 6) (3, 2))]+ }+scroll = ItemKind+ { isymbol = '?'+ , iname = "scroll"+ , ifreq = [("useful", 4)]+ , iflavour = zipFancy darkCol -- arcane and old+ , icount = intToDeep 1+ , iverbApply = "decipher"+ , iverbProject = "lob"+ , iweight = 50+ , itoThrow = -75 -- bad shape, even rolled up+ , ifeature = []+ }+scroll1 = scroll+ { ifreq = [("useful", 2)]+ , ifeature = [Cause $ CallFriend 1]+ }+scroll2 = scroll+ { ifeature = [Cause $ Summon 1]+ }+scroll3 = scroll+ { ifeature = [Cause $ Ascend (-1)]+ }+sword = ItemKind+ { isymbol = ')'+ , iname = "sword"+ , ifreq = [("useful", 40)]+ , iflavour = zipPlain [BrCyan]+ , icount = intToDeep 1+ , iverbApply = "hit"+ , iverbProject = "heave"+ , iweight = 2000+ , itoThrow = -50 -- ensuring it hits with the tip costs speed+ , ifeature = [Cause $ Hurt (rollDice 5 1) (rollDeep (1, 2) (4, 2))]+ }+wand = ItemKind+ { isymbol = '/'+ , iname = "wand"+ , ifreq = [("useful", 5)]+ , iflavour = zipFancy brightCol+ , icount = intToDeep 1+ , iverbApply = "snap"+ , iverbProject = "zap"+ , iweight = 300+ , itoThrow = 25 -- magic+ , ifeature = [Fragile]+ }+wand1 = wand+ { ifeature = ifeature wand ++ [Cause Dominate]+ }+wand2 = wand+ { ifreq = [("useful", 2)]+ , ifeature = ifeature wand ++ [Cause $ Heal (-25)]+ }+fist = sword+ { isymbol = '@'+ , iname = "fist"+ , ifreq = [("hth", 1), ("unarmed", 100)]+ , iverbApply = "punch"+ , iverbProject = "ERROR, please report: iverbProject fist"+ , ifeature = [Cause $ Hurt (rollDice 5 1) (intToDeep 0)]+ }+foot = sword+ { isymbol = '@'+ , iname = "foot"+ , ifreq = [("hth", 1), ("unarmed", 50)]+ , iverbApply = "kick"+ , iverbProject = "ERROR, please report: iverbProject foot"+ , ifeature = [Cause $ Hurt (rollDice 5 1) (intToDeep 0)]+ }+tentacle = sword+ { isymbol = 'S'+ , iname = "tentacle"+ , ifreq = [("hth", 1), ("monstrous", 100)]+ , iverbApply = "hit"+ , iverbProject = "ERROR, please report: iverbProject tentacle"+ , ifeature = [Cause $ Hurt (rollDice 5 1) (intToDeep 0)]+ }+fragrance = ItemKind+ { isymbol = '\''+ , iname = "fragrance"+ , ifreq = [("fragrance", 1)]+ , iflavour = zipFancy [BrMagenta]+ , icount = rollDeep (5, 2) (0, 0)+ , iverbApply = "smell"+ , iverbProject = "exude"+ , iweight = 1+ , itoThrow = -93 -- the slowest that gets anywhere (1 step only)+ , ifeature = [Fragile]+ }+mist_healing = ItemKind+ { isymbol = '\''+ , iname = "mist"+ , ifreq = [("mist healing", 1)]+ , iflavour = zipFancy [White]+ , icount = rollDeep (12, 2) (0, 0)+ , iverbApply = "inhale"+ , iverbProject = "blow"+ , iweight = 1+ , itoThrow = -87 -- the slowest that travels at least 2 steps+ , ifeature = [Cause $ Heal 1, Fragile]+ }+mist_wounding = ItemKind+ { isymbol = '\''+ , iname = "mist"+ , ifreq = [("mist wounding", 1)]+ , iflavour = zipFancy [White]+ , icount = rollDeep (12, 2) (0, 0)+ , iverbApply = "inhale"+ , iverbProject = "blow"+ , iweight = 1+ , itoThrow = -87+ , ifeature = [Cause $ Heal (-1), Fragile]+ }+glass_piece = ItemKind+ { isymbol = '\''+ , iname = "glass piece"+ , ifreq = [("glass piece", 1)]+ , iflavour = zipPlain [BrBlue]+ , icount = rollDeep (10, 2) (0, 0)+ , iverbApply = "grate"+ , iverbProject = "toss"+ , iweight = 10+ , itoThrow = 0+ , ifeature = [Cause $ Hurt (rollDice 1 1) (intToDeep 0), Fragile, Linger 20]+ }+smoke = ItemKind+ { isymbol = '\''+ , iname = "smoke"+ , ifreq = [("smoke", 1)]+ , iflavour = zipPlain [BrBlack]+ , icount = rollDeep (12, 2) (0, 0)+ , iverbApply = "inhale"+ , iverbProject = "blow"+ , iweight = 1+ , itoThrow = -70+ , ifeature = [Fragile]+ }
+ GameDefinition/Content/ModeKind.hs view
@@ -0,0 +1,301 @@+-- | The type of kinds of game modes for LambdaHack.+module Content.ModeKind ( cdefs ) where++import qualified Data.EnumMap.Strict as EM++import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Content.ModeKind++cdefs :: ContentDef ModeKind+cdefs = ContentDef+ { getSymbol = msymbol+ , getName = mname+ , getFreq = mfreq+ , validate = validateModeKind+ , content =+ [campaign, skirmish, battle, pvp, coop, defense, testCampaign, testSkirmish, testBattle, testPvP, testCoop, testDefense, peekCampaign, peekSkirmish]+ }+campaign, skirmish, battle, pvp, coop, defense, testCampaign, testSkirmish, testBattle, testPvP, testCoop, testDefense, peekCampaign, peekSkirmish :: ModeKind++campaign = ModeKind+ { msymbol = 'r'+ , mname = "campaign"+ , mfreq = [("campaign", 1)]+ , mplayers = playersCampaign+ , mcaves = cavesCampaign+ }++skirmish = ModeKind+ { msymbol = 'k'+ , mname = "skirmish"+ , mfreq = [("skirmish", 1)]+ , mplayers = playersSkirmish+ , mcaves = cavesCombat+ }++battle = ModeKind+ { msymbol = 'b'+ , mname = "battle"+ , mfreq = [("battle", 1)]+ , mplayers = playersBattle+ , mcaves = cavesBattle+ }++pvp = ModeKind+ { msymbol = 'v'+ , mname = "PvP"+ , mfreq = [("PvP", 1)]+ , mplayers = playersPvP+ , mcaves = cavesCombat+ }++coop = ModeKind+ { msymbol = 'o'+ , mname = "Coop"+ , mfreq = [("Coop", 1)]+ , mplayers = playersCoop+ , mcaves = cavesCampaign+ }++defense = ModeKind+ { msymbol = 'e'+ , mname = "defense"+ , mfreq = [("defense", 1)]+ , mplayers = playersDefense+ , mcaves = cavesCampaign+ }++testCampaign = ModeKind+ { msymbol = 't'+ , mname = "testCampaign"+ , mfreq = [("testCampaign", 1)]+ , mplayers = playersTestCampaign+ , mcaves = cavesCampaign+ }++testSkirmish = ModeKind+ { msymbol = 't'+ , mname = "testSkirmish"+ , mfreq = [("testSkirmish", 1)]+ , mplayers = playersTestSkirmish+ , mcaves = cavesCombat+ }++testBattle = ModeKind+ { msymbol = 't'+ , mname = "testBattle"+ , mfreq = [("testBattle", 1)]+ , mplayers = playersTestBattle+ , mcaves = cavesBattle+ }++testPvP = ModeKind+ { msymbol = 't'+ , mname = "testPvP"+ , mfreq = [("testPvP", 1)]+ , mplayers = playersTestPvP+ , mcaves = cavesCombat+ }++testCoop = ModeKind+ { msymbol = 't'+ , mname = "testCoop"+ , mfreq = [("testCoop", 1)]+ , mplayers = playersTestCoop+ , mcaves = cavesCampaign+ }++testDefense = ModeKind+ { msymbol = 't'+ , mname = "testDefense"+ , mfreq = [("testDefense", 1)]+ , mplayers = playersTestDefense+ , mcaves = cavesCampaign+ }++peekCampaign = ModeKind+ { msymbol = 'p'+ , mname = "peekCampaign"+ , mfreq = [("peekCampaign", 1)]+ , mplayers = playersPeekCampaign+ , mcaves = cavesCampaign+ }++peekSkirmish = ModeKind+ { msymbol = 'p'+ , mname = "peekSkirmish"+ , mfreq = [("peekSkirmish", 1)]+ , mplayers = playersPeekSkirmish+ , mcaves = cavesCombat+ }+++playersCampaign, playersSkirmish, playersBattle, playersPvP, playersCoop, playersDefense, playersTestCampaign, playersTestSkirmish, playersTestBattle, playersTestPvP, playersTestCoop, playersTestDefense, playersPeekCampaign, playersPeekSkirmish :: Players++playersCampaign = Players+ { playersList = [ playerHero {playerInitial = 1}+ , playerMonster ]+ , playersEnemy = [("Adventurer Party", "Monster Hive")]+ , playersAlly = [] }++playersSkirmish = Players+ { playersList = [ playerHero {playerName = "White"}+ , playerAntiHero {playerName = "Purple"}+ , playerHorror ]+ , playersEnemy = [ ("White", "Purple")+ , ("White", "Horror Den")+ , ("Purple", "Horror Den") ]+ , playersAlly = [] }++playersBattle = Players+ { playersList = [ playerHero {playerInitial = 5}+ , playerMonster { playerInitial = 30+ , playerSpawn = 0 } ]+ , playersEnemy = [("Adventurer Party", "Monster Hive")]+ , playersAlly = [] }++playersPvP = Players+ { playersList = [ playerHero {playerName = "Red"}+ , playerHero {playerName = "Blue"}+ , playerHorror ]+ , playersEnemy = [ ("Red", "Blue")+ , ("Red", "Horror Den")+ , ("Blue", "Horror Den") ]+ , playersAlly = [] }++playersCoop = Players+ { playersList = [ playerHero { playerName = "Coral"+ , playerInitial = 1 }+ , playerHero { playerName = "Amber"+ , playerInitial = 1 }+ , playerMonster ]+ , playersEnemy = [ ("Coral", "Monster Hive")+ , ("Amber", "Monster Hive") ]+ , playersAlly = [("Coral", "Amber")] }++playersDefense = Players+ { playersList = [ playerMonster { playerInitial = 1+ , playerAiLeader = False+ , playerHuman = True+ , playerUI = True }+ , playerAntiHero {playerName = "Green"}+ , playerAntiHero {playerName = "Yellow"}+ , playerAntiHero {playerName = "Cyan"} ]+ , playersEnemy = [ ("Green", "Monster Hive")+ , ("Yellow", "Monster Hive")+ , ("Cyan", "Monster Hive") ]+ , playersAlly = [ ("Green", "Yellow")+ , ("Green", "Cyan")+ , ("Yellow", "Cyan") ] }++playersTestCampaign = playersCampaign+ { playersList = [ playerHero { playerInitial = 5+ , playerAiLeader = True+ , playerHuman = False }+ , playerMonster ] }++playersTestSkirmish = playersSkirmish+ { playersList = [ playerHero { playerName = "White"+ , playerAiLeader = True+ , playerHuman = False }+ , playerAntiHero { playerName = "Purple" }+ , playerHorror ] }++playersTestBattle = playersBattle+ { playersList = [ playerHero { playerInitial = 5+ , playerAiLeader = True+ , playerHuman = False }+ , playerMonster { playerInitial = 30+ , playerSpawn = 0 } ] }++playersTestPvP = playersPvP+ { playersList = [ playerHero { playerName = "Red"+ , playerAiLeader = True+ , playerHuman = False }+ , playerHero { playerName = "Blue"+ , playerAiLeader = True+ , playerHuman = False }+ , playerHorror ] }++playersTestCoop = playersCoop+ { playersList = [ playerHero { playerName = "Coral"+ , playerAiLeader = True+ , playerHuman = False }+ , playerHero { playerName = "Amber"+ , playerAiLeader = True+ , playerHuman = False }+ , playerMonster ] }++playersTestDefense = playersDefense+ { playersList = [ playerMonster { playerInitial = 1+ , playerUI = True }+ , playerAntiHero {playerName = "Green"}+ , playerAntiHero {playerName = "Yellow"}+ , playerAntiHero {playerName = "Cyan"} ] }++playersPeekCampaign = playersCampaign+ { playersList = [ playerHero {playerInitial = 1}+ , playerMonster {playerUI = True} ] }++playersPeekSkirmish = playersSkirmish+ { playersList = [ playerHero {playerName = "White"}+ , playerAntiHero { playerName = "Purple"+ , playerUI = True }+ , playerHorror ] }+++playerHero, playerAntiHero, playerMonster, playerHorror :: Player++playerHero = Player+ { playerName = "Adventurer Party"+ , playerFaction = "hero"+ , playerSpawn = 0+ , playerEntry = toEnum (-1)+ , playerInitial = 3+ , playerAiLeader = False+ , playerAiOther = True+ , playerHuman = True+ , playerUI = True+ }++playerAntiHero = playerHero+ { playerAiLeader = True+ , playerHuman = False+ , playerUI = False+ }++playerMonster = Player+ { playerName = "Monster Hive"+ , playerFaction = "monster"+ , playerSpawn = 50+ , playerEntry = toEnum (-3)+ , playerInitial = 5+ , playerAiLeader = True+ , playerAiOther = True+ , playerHuman = False+ , playerUI = False+ }++playerHorror = Player+ { playerName = "Horror Den"+ , playerFaction = "horror"+ , playerSpawn = 0+ , playerEntry = toEnum (-1)+ , playerInitial = 0+ , playerAiLeader = True+ , playerAiOther = True+ , playerHuman = False+ , playerUI = False+ }+++cavesCampaign, cavesCombat, cavesBattle :: Caves++cavesCampaign = EM.fromList [ (toEnum (-1), ("caveRogue", Just True))+ , (toEnum (-2), ("caveRogue", Nothing))+ , (toEnum (-3), ("caveEmpty", Nothing))+ , (toEnum (-10), ("caveNoise", Nothing))]++cavesCombat = EM.fromList [(toEnum (-3), ("caveCombat", Nothing))]++cavesBattle = EM.fromList [(toEnum (-3), ("caveBattle", Nothing))]
+ GameDefinition/Content/PlaceKind.hs view
@@ -0,0 +1,71 @@+-- | Rooms, halls and passages for LambdaHack.+module Content.PlaceKind ( cdefs ) where++import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Content.PlaceKind++cdefs :: ContentDef PlaceKind+cdefs = ContentDef+ { getSymbol = psymbol+ , getName = pname+ , getFreq = pfreq+ , validate = validatePlaceKind+ , content =+ [rect, pillar, pillarC, pillar3, colonnade, colonnadeW]+ }+rect, pillar, pillarC, pillar3, colonnade, colonnadeW :: PlaceKind++rect = PlaceKind -- Valid for any nonempty area, hence low frequency.+ { psymbol = 'r'+ , pname = "room"+ , pfreq = [("rogue", 100)]+ , pcover = CStretch+ , pfence = FNone+ , ptopLeft = [ "--"+ , "|."+ ]+ }+pillar = PlaceKind+ { psymbol = 'p'+ , pname = "pillar room"+ , pfreq = [("rogue", 1000)] -- larger rooms require support pillars+ , pcover = CStretch+ , pfence = FNone+ , ptopLeft = [ "-----"+ , "|...."+ , "|.O.."+ , "|...."+ , "|...."+ ]+ }+pillarC = pillar+ { ptopLeft = [ "-----"+ , "|O..."+ , "|...."+ , "|...."+ , "|...."+ ]+ }+pillar3 = pillar+ { ptopLeft = [ "-----"+ , "|&.O."+ , "|...."+ , "|O..."+ , "|...."+ ]+ }+colonnade = PlaceKind+ { psymbol = 'c'+ , pname = "colonnade"+ , pfreq = [("rogue", 500)]+ , pcover = CAlternate+ , pfence = FFloor+ , ptopLeft = [ "O."+ , ".O"+ ]+ }+colonnadeW = colonnade+ { ptopLeft = [ "O."+ , ".."+ ]+ }
+ GameDefinition/Content/RuleKind.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE TemplateHaskell #-}+-- | Game rules and assorted game setup data for LambdaHack.+module Content.RuleKind ( cdefs ) where++import Control.Arrow (first)+import Language.Haskell.TH.Syntax+import System.FilePath++-- Cabal+import qualified Paths_LambdaHack as Self (getDataFileName, version)++import Game.LambdaHack.Common.ContentDef+import qualified Game.LambdaHack.Common.Effect as Effect+import qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.HumanCmd+import qualified Game.LambdaHack.Common.Key as K+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.RuleKind++cdefs :: ContentDef RuleKind+cdefs = ContentDef+ { getSymbol = rsymbol+ , getName = rname+ , getFreq = rfreq+ , validate = validateRuleKind+ , content =+ [standard]+ }++standard :: RuleKind+standard = RuleKind+ { rsymbol = 's'+ , rname = "standard LambdaHack ruleset"+ , rfreq = [("standard", 100)]+ -- Check whether one position is accessible from another.+ -- Precondition: the two positions are next to each other.+ -- Apart of checking the target tile, we forbid diagonal movement+ -- to and from doors.+ , raccessible = Nothing+ , raccessibleDoor = Just $ \spos tpos ->+ not $ isDiagonal $ displacement spos tpos+ , rtitle = "LambdaHack"+ , rpathsDataFile = Self.getDataFileName+ , rpathsVersion = Self.version+ , ritemMelee = ")"+ , ritemRanged = "|"+ -- Wasting weapons and armour would be too cruel to the player.+ , ritemProject = "!?|/"+ -- The strings containing the default configuration file+ -- included from config.ui.default.+ , rcfgUIName = "config.ui"+ , rcfgUIDefault = $(do+ let path = "GameDefinition" </> "config.ui" <.> "default"+ qAddDependentFile path+ x <- qRunIO (readFile path)+ lift x)+ -- 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 characters) that is ignored.+ -- For a different screen size, the picture is centered and the outermost+ -- rows and columns cloned. When displayed in the Main Menu screen,+ -- it's overwritten with the game version string and keybinding strings.+ -- The game version string begins and ends with a space and is placed+ -- in the very bottom right corner. The keybindings overwrite places+ -- marked with 25 left curly brace signs '{' in a row. The sign is forbidden+ -- everywhere else. A specific number of such places with 25 left braces+ -- are required, at most one per row, and all are overwritten+ -- with text that is flushed left and padded with spaces.+ -- The Main Menu is displayed dull white on black.+ -- TODO: Show highlighted keybinding in inverse video or bright white on grey+ -- background. The spaces that pad keybindings are not highlighted.+ , rmainMenuArt = $(do+ let path = "GameDefinition/MainMenu.ascii"+ qAddDependentFile path+ x <- qRunIO (readFile path)+ lift x)+ , rhumanCommands = map (first K.mkKM)+ -- All commands are defined here, except some movement and leader picking+ -- commands. All commands are shown on help screens except debug commands+ -- and macros with empty descriptions.+ -- The order below determines the order on the help screens.+ -- Remember to put commands that show information (e.g., enter targeting+ -- mode) first.++ -- Main Menu, which apart of these includes a few extra commands+ [ ("CTRL-x", (CmdMenu, GameExit))+ , ("CTRL-r", (CmdMenu, GameRestart "campaign"))+ , ("CTRL-k", (CmdMenu, GameRestart "skirmish"))+ , ("CTRL-v", (CmdMenu, GameRestart "PvP"))+ , ("CTRL-o", (CmdMenu, GameRestart "Coop"))+ , ("CTRL-e", (CmdMenu, GameRestart "defense"))+ , ("CTRL-d", (CmdMenu, GameDifficultyCycle))++ -- Movement and terrain alteration+ , ("less", (CmdMove, TriggerTile+ [ TriggerFeature { verb = "ascend"+ , object = "a level"+ , feature = F.Cause (Effect.Ascend 1) }+ , TriggerFeature { verb = "escape"+ , object = "dungeon"+ , feature = F.Cause (Effect.Escape 1) } ]))+ , ("CTRL-less", (CmdMove, TriggerTile+ [ TriggerFeature { verb = "ascend"+ , object = "10 levels"+ , feature = F.Cause (Effect.Ascend 10) } ]))+ , ("greater", (CmdMove, TriggerTile+ [ TriggerFeature { verb = "descend"+ , object = "a level"+ , feature = F.Cause (Effect.Ascend (-1)) }+ , TriggerFeature { verb = "escape"+ , object = "dungeon"+ , feature = F.Cause (Effect.Escape (-1)) } ]))+ , ("CTRL-greater", (CmdMove, TriggerTile+ [ TriggerFeature { verb = "descend"+ , object = "10 levels"+ , feature = F.Cause (Effect.Ascend (-10)) } ]))+ , ("CTRL-semicolon", (CmdMove, StepToTarget))+ , ("semicolon", (CmdMove, Macro "go to target for 100 steps"+ ["CTRL-semicolon", "P"]))+ , ("x", (CmdMove, Macro "explore the closest unknown spot"+ [ "BackSpace"+ , "CTRL-question", "CTRL-semicolon", "P" ]))+ , ("X", (CmdMove, Macro "autoexplore 100 times"+ [ "BackSpace"+ , "'", "CTRL-question", "CTRL-semicolon", "'"+ , "P" ]))+ , ("R", (CmdMove, Macro "rest (wait 100 times)" ["KP_Begin", "P"]))+ , ("c", (CmdMove, AlterDir+ [ AlterFeature { verb = "close"+ , object = "door"+ , feature = F.CloseTo "vertical closed door Lit" }+ , AlterFeature { verb = "close"+ , object = "door"+ , feature = F.CloseTo "horizontal closed door Lit" }+ , AlterFeature { verb = "close"+ , object = "door"+ , feature = F.CloseTo "vertical closed door Dark" }+ , AlterFeature { verb = "close"+ , object = "door"+ , feature = F.CloseTo "horizontal closed door Dark" }+ ]))+ , ("o", (CmdMove, AlterDir+ [ AlterFeature { verb = "open"+ , object = "door"+ , feature = F.OpenTo "vertical open door Lit" }+ , AlterFeature { verb = "open"+ , object = "door"+ , feature = F.OpenTo "horizontal open door Lit" }+ , AlterFeature { verb = "open"+ , object = "door"+ , feature = F.OpenTo "vertical open door Dark" }+ , AlterFeature { verb = "open"+ , object = "door"+ , feature = F.OpenTo "horizontal open door Dark" }+ ]))++ -- Inventory and items+ , ("I", (CmdItem, Inventory))+ , ("g", (CmdItem, Pickup))+ , ("d", (CmdItem, Drop))+ , ("q", (CmdItem, Apply [ApplyItem { verb = "quaff"+ , object = "potion"+ , symbol = '!' }]))+ , ("r", (CmdItem, Apply [ApplyItem { verb = "read"+ , object = "scroll"+ , symbol = '?' }]))+ , ("t", (CmdItem, Project [ApplyItem { verb = "throw"+ , object = "missile"+ , symbol = '|' }]))+ , ("z", (CmdItem, Project [ApplyItem { verb = "zap"+ , object = "wand"+ , symbol = '/' }]))++ -- Targeting+ , ("asterisk", (CmdTgt, TgtEnemy))+ , ("slash", (CmdTgt, TgtFloor))+ , ("plus", (CmdTgt, EpsIncr True))+ , ("minus", (CmdTgt, EpsIncr False))+ , ("BackSpace", (CmdTgt, TgtClear))+ , ("CTRL-question", (CmdTgt, TgtUnknown))+ , ("CTRL-I", (CmdTgt, TgtItem))+ , ("CTRL-braceleft", (CmdTgt, TgtStair True))+ , ("CTRL-braceright", (CmdTgt, TgtStair False))++ -- Assorted+ , ("question", (CmdMeta, Help))+ , ("D", (CmdMeta, History))+ , ("T", (CmdMeta, MarkSuspect))+ , ("V", (CmdMeta, MarkVision))+ , ("S", (CmdMeta, MarkSmell))+ , ("Tab", (CmdMeta, MemberCycle))+ , ("ISO_Left_Tab", (CmdMeta, MemberBack))+ , ("equal", (CmdMeta, SelectActor))+ , ("underscore", (CmdMeta, SelectNone))+ , ("p", (CmdMeta, Repeat 1))+ , ("P", (CmdMeta, Repeat 100))+ , ("CTRL-p", (CmdMeta, Repeat 1000))+ , ("apostrophe", (CmdMeta, Record))+ , ("space", (CmdMeta, Clear))+ , ("Escape", (CmdMeta, Cancel))+ , ("Return", (CmdMeta, Accept))++ -- Debug and others not to display in help screens+ , ("CTRL-s", (CmdDebug, GameSave))+ , ("CTRL-y", (CmdDebug, Resend))+ ]+ , rfirstDeathEnds = False+ , rfovMode = Digital 12+ , rsaveBkpClips = 500+ , rleadLevelClips = 100+ , rscoresFile = "scores"+ , rsavePrefix = "save"+ }
+ GameDefinition/Content/TileKind.hs view
@@ -0,0 +1,260 @@+-- | Terrain tiles for LambdaHack.+module Content.TileKind ( cdefs ) where++import Control.Arrow (first)+import Data.Text (Text)+import qualified Data.Text as T++import Game.LambdaHack.Common.Color+import Game.LambdaHack.Common.ContentDef+import qualified Game.LambdaHack.Common.Effect as Effect+import Game.LambdaHack.Common.Feature+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Content.TileKind++cdefs :: ContentDef TileKind+cdefs = ContentDef+ { getSymbol = tsymbol+ , getName = tname+ , getFreq = tfreq+ , validate = validateTileKind+ , content =+ [wall, hardRock, pillar, pillarCache, tree, wallV, wallSuspectV, doorClosedV, doorOpenV, wallH, wallSuspectH, doorClosedH, doorOpenH, stairsUpLit, stairsLit, stairsDownLit, escapeUpLit, escapeDownLit, unknown, floorCorridorLit, floorArenaLit, floorActorLit, floorItemLit, floorActorItemLit, floorRedLit, floorBlueLit, floorGreenLit, floorBrownLit]+ ++ map makeDark [wallV, wallSuspectV, doorClosedV, doorOpenV, wallH, wallSuspectH, doorClosedH, doorOpenH, stairsLit, escapeUpLit, escapeDownLit, floorCorridorLit]+ ++ map makeDarkColor [stairsUpLit, stairsDownLit, floorArenaLit, floorActorLit, floorItemLit, floorActorItemLit]+ }+wall, hardRock, pillar, pillarCache, tree, wallV, wallSuspectV, doorClosedV, doorOpenV, wallH, wallSuspectH, doorClosedH, doorOpenH, stairsUpLit, stairsLit, stairsDownLit, escapeUpLit, escapeDownLit, unknown, floorCorridorLit, floorArenaLit, floorActorLit, floorItemLit, floorActorItemLit, floorRedLit, floorBlueLit, floorGreenLit, floorBrownLit :: TileKind++wall = TileKind+ { tsymbol = ' '+ , tname = "bedrock"+ , tfreq = [("fillerWall", 1), ("legendLit", 100), ("legendDark", 100)]+ , tcolor = defBG+ , tcolor2 = defBG+ , tfeature = [Dark]+ }+hardRock = TileKind+ { tsymbol = ' '+ , tname = "impenetrable bedrock"+ , tfreq = [("basic outer fence", 1)]+ , tcolor = BrWhite+ , tcolor2 = BrWhite+ , tfeature = [Dark, Impenetrable]+ }+pillar = TileKind+ { tsymbol = 'O'+ , tname = "rock"+ , tfreq = [ ("cachable", 70)+ , ("legendLit", 100), ("legendDark", 100)+ , ("noiseSet", 55), ("combatSet", 3), ("battleSet", 9) ]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = []+ }+pillarCache = TileKind+ { tsymbol = '&'+ , tname = "cache"+ , tfreq = [ ("cachable", 30)+ , ("legendLit", 100), ("legendDark", 100) ]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [Suspect, Cause $ Effect.CreateItem 1, ChangeTo "cachable"]+ }+tree = TileKind+ { tsymbol = 'O'+ , tname = "tree"+ , tfreq = [("combatSet", 8)]+ , tcolor = BrGreen+ , tcolor2 = Green+ , tfeature = []+ }+wallV = TileKind+ { tsymbol = '|'+ , tname = "granite wall"+ , tfreq = [("legendLit", 100)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [HideAs "suspect vertical wall Lit"]+ }+wallSuspectV = TileKind+ { tsymbol = '|'+ , tname = "moldy wall"+ , tfreq = [("suspect vertical wall Lit", 1)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [Suspect, RevealAs "vertical closed door Lit"]+ }+doorClosedV = TileKind+ { tsymbol = '+'+ , tname = "closed door"+ , tfreq = [("vertical closed door Lit", 1)]+ , tcolor = Brown+ , tcolor2 = BrBlack+ , tfeature = [ OpenTo "vertical open door Lit"+ , HideAs "suspect vertical wall Lit"+ ]+ }+doorOpenV = TileKind+ { tsymbol = '-'+ , tname = "open door"+ , tfreq = [("vertical open door Lit", 1)]+ , tcolor = Brown+ , tcolor2 = BrBlack+ , tfeature = [ Walkable, Clear+ , CloseTo "vertical closed door Lit"+ ]+ }+wallH = TileKind+ { tsymbol = '-'+ , tname = "granite wall"+ , tfreq = [("legendLit", 100)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [HideAs "suspect horizontal wall Lit"]+ }+wallSuspectH = TileKind+ { tsymbol = '-'+ , tname = "scratched wall"+ , tfreq = [("suspect horizontal wall Lit", 1)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [Suspect, RevealAs "horizontal closed door Lit"]+ }+doorClosedH = TileKind+ { tsymbol = '+'+ , tname = "closed door"+ , tfreq = [("horizontal closed door Lit", 1)]+ , tcolor = Brown+ , tcolor2 = BrBlack+ , tfeature = [ OpenTo "horizontal open door Lit"+ , HideAs "suspect horizontal wall Lit"+ ]+ }+doorOpenH = TileKind+ { tsymbol = '|'+ , tname = "open door"+ , tfreq = [("horizontal open door Lit", 1)]+ , tcolor = Brown+ , tcolor2 = BrBlack+ , tfeature = [ Walkable, Clear+ , CloseTo "horizontal closed door Lit"+ ]+ }+stairsUpLit = TileKind+ { tsymbol = '<'+ , tname = "staircase up"+ , tfreq = [("legendLit", 100)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [Walkable, Clear, Cause $ Effect.Ascend 1]+ }+stairsLit = TileKind+ { tsymbol = '>'+ , tname = "staircase"+ , tfreq = [("legendLit", 100)]+ , tcolor = BrCyan+ , tcolor2 = Cyan -- TODO+ , tfeature = [ Walkable, Clear+ , Cause $ Effect.Ascend 1+ , Cause $ Effect.Ascend (-1) ]+ }+stairsDownLit = TileKind+ { tsymbol = '>'+ , tname = "staircase down"+ , tfreq = [("legendLit", 100)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [Walkable, Clear, Cause $ Effect.Ascend (-1)]+ }+escapeUpLit = TileKind+ { tsymbol = '<'+ , tname = "exit trapdoor up"+ , tfreq = [("legendLit", 100)]+ , tcolor = BrYellow+ , tcolor2 = BrYellow+ , tfeature = [Walkable, Clear, Cause (Effect.Escape 1)]+ }+escapeDownLit = TileKind+ { tsymbol = '>'+ , tname = "exit trapdoor down"+ , tfreq = [("legendLit", 100)]+ , tcolor = BrYellow+ , tcolor2 = BrYellow+ , tfeature = [Walkable, Clear, Cause (Effect.Escape (-1))]+ }+unknown = TileKind+ { tsymbol = ' '+ , tname = "unknown space"+ , tfreq = [("unknown space", 1)]+ , tcolor = defFG+ , tcolor2 = defFG+ , tfeature = [Dark]+ }+floorCorridorLit = TileKind+ { tsymbol = '#'+ , tname = "corridor"+ , tfreq = [("floorCorridorLit", 1)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [Walkable, Clear]+ }+floorArenaLit = floorCorridorLit+ { tsymbol = '.'+ , tname = "stone floor"+ , tfreq = [ ("floorArenaLit", 1)+ , ("arenaSet", 1), ("noiseSet", 100), ("combatSet", 100) ]+ }+floorActorLit = floorArenaLit+ { tfreq = [("floorActorLit", 1), ("battleSet", 100)]+ , tfeature = CanActor : tfeature floorArenaLit+ }+floorItemLit = floorArenaLit+ { tfreq = []+ , tfeature = CanItem : tfeature floorArenaLit+ }+floorActorItemLit = floorItemLit+ { tfreq = [("legendLit", 100), ("emptySet", 1)]+ , tfeature = CanActor : tfeature floorItemLit+ }+floorRedLit = floorArenaLit+ { tname = "brick pavement"+ , tfreq = [("trailLit", 30)]+ , tcolor = BrRed+ , tcolor2 = Red+ , tfeature = Trail : tfeature floorArenaLit+ }+floorBlueLit = floorRedLit+ { tname = "granite cobblestones"+ , tfreq = [("trailLit", 100)]+ , tcolor = BrBlue+ , tcolor2 = Blue+ }+floorGreenLit = floorRedLit+ { tname = "mossy stone path"+ , tfreq = [("trailLit", 100)]+ , tcolor = BrGreen+ , tcolor2 = Green+ }+floorBrownLit = floorRedLit+ { tname = "rotting mahogany deck"+ , tfreq = [("trailLit", 10)]+ , tcolor = BrMagenta+ , tcolor2 = Magenta+ }++makeDark :: TileKind -> TileKind+makeDark k = let textLit :: Text -> Text+ textLit t = maybe t (<> "Dark") $ T.stripSuffix "Lit" t+ litFreq = map (first textLit) $ tfreq k+ litFeat (OpenTo t) = OpenTo $ textLit t+ litFeat (CloseTo t) = CloseTo $ textLit t+ litFeat (ChangeTo t) = ChangeTo $ textLit t+ litFeat (HideAs t) = HideAs $ textLit t+ litFeat (RevealAs t) = RevealAs $ textLit t+ litFeat feat = feat+ in k { tfreq = litFreq+ , tfeature = Dark : map litFeat (tfeature k)+ }++makeDarkColor :: TileKind -> TileKind+makeDarkColor k = (makeDark k) {tcolor2 = BrBlack}
+ GameDefinition/Main.hs view
@@ -0,0 +1,50 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+-- | The main source code file of LambdaHack. Here the knot of engine+-- code pieces and the LambdaHack-specific content defintions is tied,+-- resulting in an executable game.+module Main ( main ) where++import qualified Content.ActorKind+import qualified Content.CaveKind+import qualified Content.FactionKind+import qualified Content.ItemKind+import qualified Content.ModeKind+import qualified Content.PlaceKind+import qualified Content.RuleKind+import qualified Content.TileKind+import Game.LambdaHack.Client+import Game.LambdaHack.Client.Action.ActionType+import Game.LambdaHack.Common.Action (MonadAtomic (..))+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.AtomicSem+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Server+import Game.LambdaHack.Server.Action.ActionType+import Game.LambdaHack.Server.AtomicSemSer++-- | The game-state semantics of atomic game commands+-- as computed on the server.+instance MonadAtomic ActionSer where+ execAtomic = atomicSendSem++-- | The game-state semantics of atomic game commands+-- as computed on clients. Special effects (@SfxAtomic@) don't modify state.+instance MonadAtomic (ActionCli c d) where+ execAtomic (CmdAtomic cmd) = cmdAtomicSem cmd+ execAtomic (SfxAtomic _) = return ()++-- | Tie the LambdaHack engine clients and server code+-- with the LambdaHack-specific content defintions and run the game.+main :: IO ()+main =+ let copsSlow = Kind.COps+ { coactor = Kind.createOps Content.ActorKind.cdefs+ , cocave = Kind.createOps Content.CaveKind.cdefs+ , cofaction = Kind.createOps Content.FactionKind.cdefs+ , coitem = Kind.createOps Content.ItemKind.cdefs+ , comode = Kind.createOps Content.ModeKind.cdefs+ , coplace = Kind.createOps Content.PlaceKind.cdefs+ , corule = Kind.createOps Content.RuleKind.cdefs+ , cotile = Kind.createOps Content.TileKind.cdefs+ }+ in mainSer copsSlow executorSer $ exeFrontend executorCli executorCli
+ GameDefinition/MainMenu.ascii view
@@ -0,0 +1,26 @@+----------------------------------------------------------------------------------+| |+| >> LambdaHack << |+| |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ |+| |+| |+| Version X.X.X (frontend: gtk, engine: LambdaHack X.X.X) |+----------------------------------------------------------------------------------
+ GameDefinition/PLAYING.md view
@@ -0,0 +1,204 @@+Playing LambdaHack+==================++LambdaHack is a small dungeon crawler illustrating the roguelike game engine+library also called LambdaHack. Playing the game involves walking around+the dungeon, alone or in a party of fearless adventurers, setting up ambushes,+hiding in shadow, covering tracks, breaking through to deeper caves,+bumping into monsters, doors and walls, gathering magical treasure+and making creative use of it. The bloodthirsty monsters do the same,+intelligence allowing, while tirelessly chasing the elusive heroes+by smell and sight.++Once the few basic command keys and on-screen symbols are learned,+mastery and enjoyment of the game is the matter of tactical skill+and literary imagination. To be honest, a lot of imagination is required+for this rudimentary game, but it's playable and winnable.+The game also features experimental multiplayer cooperative and competitive+modes, but they are troublesome to play with the shared-screen+and shared-keyboard interface available at this time.+Contributions welcome.+++Dungeon+-------++The heroes are marked on the map with symbols `@` and `1` through `9`.+Their goal is to explore the dungeon, battle the horrors within,+gather as much gold and gems as possible, and escape to tell the tale.+The dungeon of the campaign mode game consists of 10 levels and each level+consists of a large number of tiles. The basic tile kinds are as follows.++ dungeon terrain type on-screen symbol+ floor .+ corridor #+ wall (horizontal and vertical) - and |+ rock or tree O+ cache &+ stairs up <+ stairs down >+ open door | and -+ closed door ++ bedrock blank++The game world is persistent, i.e., every time the player visits a level+during a single game, the level layout is the same.+++Keys+----++You move throughout the level using the numerical keypad or+the vi text editor keys (also known as "Rogue-like keys").++ 7 8 9 y k u+ \|/ \|/+ 4-5-6 h-.-l+ /|\ /|\+ 1 2 3 b j n++`SHIFT` (or `CTRL`) and a movement key make the current party leader+(and currently selected party members, if any) run in the indicated+direction, until anything of interest is spotted.+The `5` and `.` keys consume a turn and make you brace for combat,+which confers a chance to block blows for the remainder of the turn.+In targeting mode the same keys move the targeting cursor.++Melee, searching for secret doors and opening closed doors+can be done by bumping into a monster, a wall and a door, respectively.+Few commands other than movement are necessary for casual play.+Some are provided only as building blocks for more complex convenience+commands, e.g., the autoexplore command (key `X`) could be defined+by the player as a macro using `BACKSPACE`, `CTRL-?`, `CTRL-;` and `P`.++Below are the remaining keys for movement and terrain alteration.++ keys command+ < ascend a level+ CTRL-< ascend 10 levels+ > descend a level+ CTRL-> descend 10 levels+ CTRL-; make one step towards the target+ ; go to target for 100 steps+ x explore the closest unknown spot+ X autoexplore 100 times+ R rest (wait 100 times)+ c close door+ o open door++Inventory and items-related keys are as follows.++ keys command+ I display inventory+ g and , get an object+ d drop an object+ q quaff potion+ r read scroll+ t throw missile+ z zap wand++To make a ranged attack, as in the last few commands above,+you need to set your target first (however, initial target is set+automatically as soon as a monster comes into view). Once in targeting mode,+you can move the targeting cursor with arrow keys and switch focus+among enemies with `*` (or among friends and enemies, depending+on targeting mode set by `/`). The details of the shared cursor position+and of the personal target are described at the bottom of the screen.+All targeting keys are listed below.++ keys command+ * target enemy+ / cycle targeting mode+ + swerve targeting line+ - unswerve targeting line+ BACKSPACE clear target/cursor+ CTRL-? target the closest unknown spot+ CTRL-I target the closest item+ CTRL-{ target the closest stairs up+ CTRL-} target the closest stairs down++Assorted remaining keys and commands follow.++ keys command+ ? display help+ D display player diary+ T mark suspect terrain+ V mark visible area+ S mark smell+ TAB cycle among party members on the level+ SHIFT-TAB cycle among party members in the dungeon+ = select (or deselect) a party member+ _ deselect (or select) all on the level+ p play back last keys+ P play back last keys 100 times+ CTRL-p play back last keys 1000 times+ ' start recording a macro+ SPACE clear messages+ ESC cancel action+ RET accept choice+ 0--9 pick a new hero leader anywhere in the dungeon++Commands for saving and exiting the current game, starting a new game, etc.,+are listed in the Main Menu, brought up by the `ESC` key.+All but the campaign and skirmish game modes are experimental+and feature multiple human or computer players (allied or not).+Game difficulty setting affects hitpoints at birth for any actors+of any UI-using faction.++ keys command+ CTRL-x save and exit+ CTRL-r new campaign game+ CTRL-k new skirmish game+ CTRL-v new PvP game+ CTRL-o new Coop game+ CTRL-e new defense game+ CTRL-d cycle next game difficulty++There are also some debug, testing and cheat options and game modes+that can be specified on the command line when starting the game server.+Use at your own peril! :) Of these, you may find the screensaver modes+the least spoilery and the most fun, e.g.:++ LambdaHack --newGame --noMore --maxFps 45 --savePrefix test --gameMode testCampaign --difficulty 1+++Monsters+--------++Heroes are not alone in the dungeon. Monsters roam the dark caves+and crawl from damp holes day and night. While heroes pay attention+to all other party members and take care to move one at a time,+monsters don't care about each other and all move at once,+sometimes brutally colliding by accident.++When the hero bumps into a monster or a monster attacks the hero,+melee combat occurs. The best weapon carried by each opponent+is taken into account for calculating bonus damage. The total damage+the current hero can potentially inflict is displayed at the bottom+of the screen. The total damage potential of a monster may change+as it finds and picks up new weapons. Heroes and monsters running into+one another (with the `SHIFT` key) do not inflict damage, but change places.+This gives the opponent a free blow, but can improve the tactical situation+or aid escape.++Throwing weapons at targets wounds them, consuming the weapon in the process.+You may throw any object in your possession (press `?` to choose+an object and press it again for a non-standard choice) or on the floor+(press `-`). Only objects of a few kinds inflict any damage.+Whenever the monster's or hero's hit points reach zero, the combatant dies.+When the last hero dies, the game ends.+++On Winning and Dying+--------------------++You win the game if you escape the dungeon alive (or eliminate+all opposition, in some game modes). Your score is+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).++If all heroes die, your score is halved and only the treasure carried+by the last standing hero counts. You are free to start again+from a different entrance to the dungeon, but all your previous wealth+is gone and fresh, undaunted enemies bar your way.
+ GameDefinition/config.ui.default view
@@ -0,0 +1,26 @@+; ; 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.++; [extra_commands]+; ; Handy shorthands with Vi keys:+; Macro_1 = ("comma", (CmdItem, Macro "" ["g"]))+; Macro_2 = ("period", (CmdMove, Macro "" ["KP_Begin"]))++; [hero_names]+; HeroName_0 = Haskell Alvin+; HeroName_1 = Alonzo Barkley+; HeroName_2 = Ines Galenti+; HeroName_3 = Ernst Abraham+; HeroName_4 = Samuel Saunders+; HeroName_5 = Roger Robin++; [ui]+; font = "Terminus,Monospace normal normal normal normal 12"+; historyMax = 5000+; maxFps = 15+; noAnim = False+; runStopMsgs = False+[dummy]
+ GameDefinition/scores view
binary file changed (absent → 188 bytes)
LICENSE view
@@ -1,5 +1,5 @@-Copyright (c) 2008--2013 Andres Loeh-Copyright (c) 2010--2013 Mikolaj Konarski+Copyright (c) 2008--2014 Andres Loeh+Copyright (c) 2010--2014 Mikolaj Konarski All rights reserved.
LambdaHack.cabal view
@@ -1,5 +1,5 @@ name: LambdaHack-version: 0.2.10.6+version: 0.2.12 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@@ -15,20 +15,11 @@ but the fundamental source of flexibility lies in the strict and type-safe separation of code and content and of clients (human and AI-controlled) and server.- .- This is a minor release, primarily intended to fix broken- vty an curses frontends and an unrelated game crash, as well as- broken builds on hackage and travis. Changes since 0.2.10- are mostly unrelated to gameplay: overhauled dungeon generation- code and the use of the external library assert-failure- for expressing assertions and error messages.- Upcoming features: new and improved frontends, improved AI- (pathfinding, autoexplore, better ranged combat), dynamic- light sources, explosions, player action undo/redo, completely- redesigned UI. Long term goals are focused on procedural- content generation and include in-game content creation,- auto-balancing and persistent content modification- based on player behaviour.+ Please see the changelog file for recent improvements+ and the issue tracker for short-term plans. Long term vision+ revolves around procedural content generation and includes+ in-game content creation, auto-balancing and persistent+ content modification based on player behaviour. . A larger game that depends on the LambdaHack library is Allure of the Stars, available from@@ -49,10 +40,11 @@ bug-reports: http://github.com/kosmikus/LambdaHack/issues license: BSD3 license-file: LICENSE-tested-with: GHC == 7.4.2, GHC == 7.6.3, GHC == 7.7-data-files: config.rules.default, config.ui.default, scores-extra-source-files: LICENSE, CREDITS, changelog, PLAYING.md, README.md,- Makefile, MainMenu.ascii, .travis.yml+tested-with: GHC == 7.6.3, GHC == 7.8+data-files: GameDefinition/config.ui.default,+ GameDefinition/scores+extra-source-files: GameDefinition/PLAYING.md, GameDefinition/MainMenu.ascii,+ README.md, LICENSE, CREDITS, changelog, Makefile author: Andres Loeh, Mikolaj Konarski maintainer: Mikolaj Konarski <mikolaj.konarski@funktory.com> category: Game Engine, Game@@ -81,7 +73,6 @@ Game.LambdaHack.Client.ClientSem, Game.LambdaHack.Client.Config, Game.LambdaHack.Client.Draw,- Game.LambdaHack.Client.HumanCmd, Game.LambdaHack.Client.HumanGlobal, Game.LambdaHack.Client.HumanLocal, Game.LambdaHack.Client.HumanSem,@@ -100,22 +91,23 @@ Game.LambdaHack.Common.AtomicSem, Game.LambdaHack.Common.ClientCmd, Game.LambdaHack.Common.Color,- Game.LambdaHack.Common.ConfigIO, Game.LambdaHack.Common.ContentDef, Game.LambdaHack.Common.Effect, Game.LambdaHack.Common.Faction, Game.LambdaHack.Common.Feature, Game.LambdaHack.Common.Flavour, Game.LambdaHack.Common.HighScore,+ Game.LambdaHack.Common.HumanCmd, Game.LambdaHack.Common.Item,+ Game.LambdaHack.Common.ItemFeature, Game.LambdaHack.Common.Key, Game.LambdaHack.Common.Kind, Game.LambdaHack.Common.Level, Game.LambdaHack.Common.Misc, Game.LambdaHack.Common.Msg, Game.LambdaHack.Common.Perception,+ Game.LambdaHack.Common.PointArray, Game.LambdaHack.Common.Point,- Game.LambdaHack.Common.PointXY, Game.LambdaHack.Common.Random, Game.LambdaHack.Common.Save, Game.LambdaHack.Common.ServerCmd,@@ -123,7 +115,6 @@ Game.LambdaHack.Common.Tile, Game.LambdaHack.Common.Time, Game.LambdaHack.Common.Vector,- Game.LambdaHack.Common.VectorXY, Game.LambdaHack.Content.ActorKind, Game.LambdaHack.Content.CaveKind, Game.LambdaHack.Content.FactionKind,@@ -140,7 +131,6 @@ Game.LambdaHack.Server.Action.ActionClass, Game.LambdaHack.Server.Action.ActionType, Game.LambdaHack.Server.AtomicSemSer,- Game.LambdaHack.Server.Config, Game.LambdaHack.Server.DungeonGen, Game.LambdaHack.Server.DungeonGen.Area Game.LambdaHack.Server.DungeonGen.AreaRnd,@@ -161,8 +151,7 @@ Game.LambdaHack.Utils.LQueue, Game.LambdaHack.Utils.Thread other-modules: Paths_LambdaHack- build-depends: ConfigFile >= 1.1.1 && < 2,- array >= 0.3.0.3 && < 1,+ build-depends: array >= 0.3.0.3 && < 1, assert-failure >= 0.1 && < 1, base >= 4 && < 5, binary >= 0.7 && < 1,@@ -173,17 +162,20 @@ enummapset-th >= 0.6.0.0 && < 1, filepath >= 1.2.0.1 && < 2, ghc-prim >= 0.2,- hashable >= 1.2 && < 2,+ hashable >= 1.1.2.5 && < 2,+ hsini >= 0.2 && < 2, keys >= 3 && < 4, miniutter >= 0.4.1 && < 2, mtl >= 2.0.1 && < 3, old-time >= 1.0.0.7 && < 2,- pretty-show >= 1.6 && < 1.6.2,+ pretty-show >= 1.6 && < 2, random >= 1.0.1 && < 2, stm >= 2.4 && < 3, text >= 0.11.2.3 && < 2, transformers >= 0.3 && < 1, unordered-containers >= 0.2.3 && < 1,+ vector >= 0.10 && < 1,+ vector-binary-instances >= 0.2 && < 1, zlib >= 0.5.3.1 && < 1 default-language: Haskell2010@@ -206,7 +198,7 @@ cpp-options: -DCURSES } else { if flag(vty) { other-modules: Game.LambdaHack.Frontend.Vty- build-depends: vty >= 4.7.0.6+ build-depends: vty >= 4.7.0.6 && < 5 cpp-options: -DVTY } else { other-modules: Game.LambdaHack.Frontend.Gtk@@ -214,7 +206,7 @@ } } executable LambdaHack- hs-source-dirs: LambdaHack+ hs-source-dirs: GameDefinition main-is: Main.hs other-modules: Content.ActorKind, Content.CaveKind,@@ -228,7 +220,6 @@ build-depends: LambdaHack, template-haskell >= 2.6 && < 3, - ConfigFile >= 1.1.1 && < 2, array >= 0.3.0.3 && < 1, assert-failure >= 0.1 && < 1, base >= 4 && < 5,@@ -240,16 +231,20 @@ enummapset-th >= 0.6.0.0 && < 1, filepath >= 1.2.0.1 && < 2, ghc-prim >= 0.2,- hashable >= 1.2 && < 2,+ hashable >= 1.1.2.5 && < 2,+ hsini >= 0.2 && < 2, keys >= 3 && < 4, miniutter >= 0.4.1 && < 2, mtl >= 2.0.1 && < 3, old-time >= 1.0.0.7 && < 2,+ pretty-show >= 1.6 && < 2, random >= 1.0.1 && < 2, stm >= 2.4 && < 3, text >= 0.11.2.3 && < 2, transformers >= 0.3 && < 1, unordered-containers >= 0.2.3 && < 1,+ vector >= 0.10 && < 1,+ vector-binary-instances >= 0.2 && < 1, zlib >= 0.5.3.1 && < 1 default-language: Haskell2010
− LambdaHack/Content/ActorKind.hs
@@ -1,88 +0,0 @@--- | Monsters and heroes for LambdaHack.-module Content.ActorKind ( cdefs ) where--import Game.LambdaHack.Common.Ability-import Game.LambdaHack.Common.Color-import Game.LambdaHack.Common.ContentDef-import Game.LambdaHack.Common.Random-import Game.LambdaHack.Common.Time-import Game.LambdaHack.Content.ActorKind--cdefs :: ContentDef ActorKind-cdefs = ContentDef- { getSymbol = asymbol- , getName = aname- , getFreq = afreq- , validate = avalidate- , content =- [hero, projectile, eye, fastEye, nose]- }-hero, projectile, eye, fastEye, nose :: ActorKind--hero = ActorKind- { asymbol = '@'- , aname = "hero"- , afreq = [("hero", 1)]- , acolor = BrWhite -- modified if many hero factions- , ahp = rollDice 50 1- , aspeed = toSpeed 2- , asight = True- , asmell = False- , aiq = 15 -- higher that that leads to looping movement- , aregen = 500- , acanDo = [minBound..maxBound]- }--projectile = ActorKind -- includes homing missiles- { asymbol = '*'- , aname = "projectile"- , afreq = [("projectile", 1)] -- Does not appear randomly in the dungeon.- , acolor = BrWhite- , ahp = rollDice 0 0- , aspeed = toSpeed 0- , asight = False- , asmell = False- , aiq = 0- , aregen = maxBound- , acanDo = [Track]- }--eye = ActorKind- { asymbol = 'e'- , aname = "reducible eye"- , afreq = [("monster", 60), ("horror", 60)]- , acolor = BrRed- , ahp = rollDice 7 4- , aspeed = toSpeed 2- , asight = True- , asmell = False- , aiq = 8- , aregen = 100- , acanDo = [minBound..maxBound]- }-fastEye = ActorKind- { asymbol = 'e'- , aname = "super-fast eye"- , afreq = [("monster", 15), ("horror", 15)]- , acolor = BrBlue- , ahp = rollDice 1 4- , aspeed = toSpeed 4- , asight = True- , asmell = False- , aiq = 12- , aregen = 5 -- Regenerates fast (at max HP most of the time!).- , acanDo = [minBound..maxBound]- }-nose = ActorKind- { asymbol = 'n'- , aname = "point-free nose"- , afreq = [("monster", 20), ("horror", 20)]- , acolor = Green- , ahp = rollDice 17 2- , aspeed = toSpeed 1.8- , asight = False- , asmell = True- , aiq = 0- , aregen = 100- , acanDo = [minBound..maxBound]- }
− LambdaHack/Content/CaveKind.hs
@@ -1,116 +0,0 @@--- | Cave layouts for LambdaHack.-module Content.CaveKind ( cdefs ) where--import Data.Ratio--import Game.LambdaHack.Common.ContentDef-import Game.LambdaHack.Common.Misc-import Game.LambdaHack.Common.Random as Random-import Game.LambdaHack.Content.CaveKind--cdefs :: ContentDef CaveKind-cdefs = ContentDef- { getSymbol = csymbol- , getName = cname- , getFreq = cfreq- , validate = cvalidate- , content =- [rogue, arena, empty, noise, combat]- }-rogue, arena, empty, noise, combat :: CaveKind--rogue = CaveKind- { csymbol = '$'- , cname = "A maze of twisty passages"- , cfreq = [("dng", 100), ("caveRogue", 1)]- , cxsize = fst normalLevelBound + 1- , cysize = snd normalLevelBound + 1- , cgrid = rollDiceXY [(3, 2)] [(1, 2), (2, 1)]- , cminPlaceSize = rollDiceXY [(2, 2), (2, 1)] [(4, 1)]- , cmaxPlaceSize = rollDiceXY [(fst normalLevelBound, 1)]- [(snd normalLevelBound, 1)]- , cdarkChance = rollDeep (1, 54) (0, 0)- , cnightChance = intToDeep 100- , cauxConnects = 1%3- , cmaxVoid = 1%6- , cminStairDist = 30- , cdoorChance = 1%2- , copenChance = 1%10- , chidden = 8- , citemNum = rollDice 7 2- , citemFreq = [(70, "useful"), (30, "treasure")]- , cdefTile = "fillerWall"- , cdarkCorTile = "floorCorridorDark"- , clitCorTile = "floorCorridorDark"- , cfillerTile = "fillerWall"- , cdarkLegendTile = "darkLegend"- , clitLegendTile = "litLegend"- }-arena = rogue- { csymbol = 'A'- , cname = "Underground city"- , cfreq = [("dng", 30), ("caveArena", 1)]- , cgrid = rollDiceXY [(2, 2)] [(2, 2)]- , cminPlaceSize = rollDiceXY [(2, 2), (3, 1)] [(4, 1)]- , cdarkChance = rollDeep (1, 80) (1, 60)- , cnightChance = intToDeep 0- , cmaxVoid = 1%3- , chidden = 1000- , citemNum = rollDice 5 2 -- few rooms- , cdefTile = "arenaSet"- , cdarkCorTile = "pathDark"- , clitCorTile = "pathLit"- }-empty = rogue- { csymbol = '.'- , cname = "Tall cavern"- , cfreq = [("dng", 20), ("caveEmpty", 1)]- , cgrid = rollDiceXY [(1, 2), (1, 1)] [(1, 1)]- , cminPlaceSize = rollDiceXY [(10, 1)] [(10, 1)]- , cmaxPlaceSize = rollDiceXY [(fst normalLevelBound * 3 `div` 5, 1)]- [(snd normalLevelBound * 3 `div` 5, 1)]- , cdarkChance = rollDeep (1, 80) (1, 80)- , cnightChance = intToDeep 0- , cauxConnects = 1- , cmaxVoid = 1%2- , cminStairDist = 50- , chidden = 1000- , citemNum = rollDice 8 2 -- whole floor strewn with treasure- , cdefTile = "emptySet"- , cdarkCorTile = "pathDark"- , clitCorTile = "floorArenaLit"- }-noise = rogue- { csymbol = '!'- , cname = "Glittering cave"- , cfreq = [("dng", 20), ("caveNoise", 1)]- , cgrid = rollDiceXY [(2, 2)] [(1, 2), (1, 1)]- , cminPlaceSize = rollDiceXY [(3, 2), (2, 1)] [(5, 1)]- , cdarkChance = rollDeep (1, 80) (1, 40)- , cnightChance = rollDeep (1, 40) (1, 40)- , cmaxVoid = 0- , chidden = 1000- , citemNum = rollDice 4 2 -- fewer rooms- , cdefTile = "noiseSet"- , cdarkCorTile = "pathDark"- , clitCorTile = "pathLit"- }-combat = rogue- { csymbol = 'C'- , cname = "Combat arena"- , cfreq = [("caveCombat", 1)]- , cgrid = rollDiceXY [(2, 2), (3, 1)] [(1, 2), (2, 1)]- , cminPlaceSize = rollDiceXY [(3, 1)] [(3, 1)]- , cmaxPlaceSize = rollDiceXY [(5, 1)] [(5, 1)]- , cdarkChance = intToDeep 100- , cnightChance = rollDeep (1, 67) (0, 0)- , chidden = 1000- , cauxConnects = 0- , cdoorChance = 1- , copenChance = 0- , citemNum = rollDice 12 2- , citemFreq = [(100, "useful")]- , cdefTile = "combatSet"- , cdarkCorTile = "pathLit" -- for now, let paths give off light- , clitCorTile = "floorArenaLit"- }
− LambdaHack/Content/FactionKind.hs
@@ -1,55 +0,0 @@--- | The type of kinds of game factions (heroes, enemies, NPCs, etc.)--- for LambdaHack.-module Content.FactionKind ( cdefs ) where--import Game.LambdaHack.Common.Ability-import Game.LambdaHack.Common.ContentDef-import Game.LambdaHack.Content.FactionKind--cdefs :: ContentDef FactionKind-cdefs = ContentDef- { getSymbol = fsymbol- , getName = fname- , getFreq = ffreq- , validate = fvalidate- , content =- [hero, monster, horror]- }-hero, monster, horror :: FactionKind--hero = FactionKind- { fsymbol = '@'- , fname = "hero"- , ffreq = [("hero", 1)]- , fAbilityLeader = allAbilities- , fAbilityOther = meleeAdjacent- }--monster = FactionKind- { fsymbol = 'm'- , fname = "monster"- , ffreq = [("monster", 1), ("spawn", 50), ("summon", 50)]- , fAbilityLeader = allAbilities- , fAbilityOther = allAbilities- }--horror = FactionKind- { fsymbol = 'h'- , fname = "horror"- , ffreq = [("horror", 1), ("summon", 50)]- , fAbilityLeader = allAbilities- , fAbilityOther = allAbilities- }---_noAbility, _onlyFollowTrack, meleeAdjacent, _meleeAndRanged, allAbilities :: [Ability]--_noAbility = [] -- not even projectiles will fly--_onlyFollowTrack = [Track] -- projectiles enabled--meleeAdjacent = [Track, Melee]--_meleeAndRanged = [Track, Melee, Ranged] -- melee and reaction fire--allAbilities = [minBound..maxBound]
− LambdaHack/Content/ItemKind.hs
@@ -1,206 +0,0 @@--- | Weapons and treasure for LambdaHack.-module Content.ItemKind ( cdefs ) where--import Game.LambdaHack.Common.Color-import Game.LambdaHack.Common.ContentDef-import Game.LambdaHack.Common.Effect-import Game.LambdaHack.Common.Flavour-import Game.LambdaHack.Common.Random-import Game.LambdaHack.Content.ItemKind--cdefs :: ContentDef ItemKind-cdefs = ContentDef- { getSymbol = isymbol- , getName = iname- , getFreq = ifreq- , validate = ivalidate- , content =- [amulet, dart, gem1, gem2, gem3, currency, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand1, wand2, fist, foot, tentacle]- }-amulet, dart, gem1, gem2, gem3, currency, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand1, wand2, fist, foot, tentacle :: ItemKind--gem, potion, scroll, wand :: ItemKind -- generic templates---- castDeep (aDb, xDy) = castDice aDb + (lvl - 1) * castDice xDy / (depth - 1)--amulet = ItemKind- { isymbol = '"'- , iname = "amulet"- , ifreq = [("useful", 6)]- , iflavour = zipFancy [BrGreen]- , ieffect = Regeneration (rollDeep (2, 3) (1, 10))- , icount = intToDeep 1- , iverbApply = "tear down"- , iverbProject = "cast"- , iweight = 30- , itoThrow = -50 -- not dense enough- }-dart = ItemKind- { isymbol = '|'- , iname = "dart"- , ifreq = [("useful", 20)]- , iflavour = zipPlain [Cyan]- , ieffect = Hurt (rollDice 1 1) (rollDeep (1, 2) (1, 2))- , icount = rollDeep (3, 3) (0, 0)- , iverbApply = "snap"- , iverbProject = "hurl"- , iweight = 50- , itoThrow = 0 -- a cheap dart- }-gem = ItemKind- { isymbol = '*'- , iname = "gem"- , ifreq = [("treasure", 20)] -- x3, but rare on shallow levels- , iflavour = zipPlain brightCol -- natural, so not fancy- , ieffect = NoEffect- , icount = intToDeep 0- , iverbApply = "crush"- , iverbProject = "toss"- , iweight = 50- , itoThrow = 0- }-gem1 = gem- { icount = rollDeep (0, 0) (1, 1) -- appears on max depth- }-gem2 = gem- { icount = rollDeep (0, 0) (1, 2) -- appears halfway- }-gem3 = gem- { icount = rollDeep (0, 0) (1, 3) -- appears early- }-currency = ItemKind- { isymbol = '$'- , iname = "gold piece"- , ifreq = [("treasure", 20), ("currency", 1)]- , iflavour = zipPlain [BrYellow]- , ieffect = NoEffect- , icount = rollDeep (0, 0) (10, 10) -- appears on lvl 2- , iverbApply = "grind"- , iverbProject = "toss"- , iweight = 31- , itoThrow = 0- }-harpoon = ItemKind- { isymbol = '|'- , iname = "harpoon"- , ifreq = [("useful", 25)]- , iflavour = zipPlain [Brown]- , ieffect = Hurt (rollDice 1 2) (rollDeep (1, 2) (2, 2))- , icount = rollDeep (0, 0) (2, 2)- , iverbApply = "break up"- , iverbProject = "hurl"- , iweight = 4000- , itoThrow = 0 -- cheap but deadly- }-potion = ItemKind- { isymbol = '!'- , iname = "potion"- , ifreq = [("useful", 15)]- , iflavour = zipFancy stdCol- , ieffect = NoEffect- , icount = intToDeep 1- , iverbApply = "gulp down"- , iverbProject = "lob"- , iweight = 200- , itoThrow = -50 -- oily, bad grip- }-potion1 = potion- { ifreq = [("useful", 5)]- , ieffect = ApplyPerfume- }-potion2 = potion- { ieffect = Heal 5- }-potion3 = potion- { ifreq = [("useful", 5)]- , ieffect = Heal (-5)- }-ring = ItemKind- { isymbol = '='- , iname = "ring"- , ifreq = [] -- [("useful", 10)] -- TODO: make it useful- , iflavour = zipPlain [White]- , ieffect = Searching (rollDeep (1, 6) (3, 2))- , icount = intToDeep 1- , iverbApply = "squeeze down"- , iverbProject = "toss"- , iweight = 15- , itoThrow = 0- }-scroll = ItemKind- { isymbol = '?'- , iname = "scroll"- , ifreq = [("useful", 4)]- , iflavour = zipFancy darkCol -- arcane and old- , ieffect = NoEffect- , icount = intToDeep 1- , iverbApply = "decipher"- , iverbProject = "lob"- , iweight = 50- , itoThrow = -75 -- bad shape, even rolled up- }-scroll1 = scroll- { ieffect = CallFriend 1- , ifreq = [("useful", 2)]- }-scroll2 = scroll- { ieffect = Summon 1- }-scroll3 = scroll- { ieffect = Ascend (-1)- }-sword = ItemKind- { isymbol = ')'- , iname = "sword"- , ifreq = [("useful", 40)]- , iflavour = zipPlain [BrCyan]- , ieffect = Hurt (rollDice 3 1) (rollDeep (1, 2) (4, 2))- , icount = intToDeep 1- , iverbApply = "hit"- , iverbProject = "heave"- , iweight = 2000- , itoThrow = -50 -- ensuring it hits with the tip costs speed- }-wand = ItemKind- { isymbol = '/'- , iname = "wand"- , ifreq = [("useful", 10)]- , iflavour = zipFancy brightCol- , ieffect = NoEffect- , icount = intToDeep 1- , iverbApply = "snap"- , iverbProject = "zap"- , iweight = 300- , itoThrow = 25 -- magic- }-wand1 = wand- { ieffect = Dominate- }-wand2 = wand- { ifreq = [("useful", 3)]- , ieffect = Heal (-25)- }-fist = sword- { isymbol = '@'- , iname = "fist"- , ifreq = [("hth", 1), ("unarmed", 100)]- , ieffect = Hurt (rollDice 3 1) (intToDeep 0)- , iverbApply = "punch"- , iverbProject = "ERROR, please report: iverbProject fist"- }-foot = sword- { isymbol = '@'- , iname = "foot"- , ifreq = [("hth", 1), ("unarmed", 50)]- , ieffect = Hurt (rollDice 3 1) (intToDeep 0)- , iverbApply = "kick"- , iverbProject = "ERROR, please report: iverbProject foot"- }-tentacle = sword- { isymbol = 'S'- , iname = "tentacle"- , ifreq = [("hth", 1), ("monstrous", 100)]- , ieffect = Hurt (rollDice 3 1) (intToDeep 0)- , iverbApply = "hit"- , iverbProject = "ERROR, please report: iverbProject tentacle"- }
− LambdaHack/Content/ModeKind.hs
@@ -1,259 +0,0 @@--- | The type of kinds of game modes for LambdaHack.-module Content.ModeKind ( cdefs ) where--import qualified Data.EnumMap.Strict as EM--import Game.LambdaHack.Common.ContentDef-import Game.LambdaHack.Content.ModeKind--cdefs :: ContentDef ModeKind-cdefs = ContentDef- { getSymbol = msymbol- , getName = mname- , getFreq = mfreq- , validate = mvalidate- , content =- [campaign, skirmish, pvp, coop, defense, screensaver, testCoop, testDefense, peekCampaign, peekSkirmish]- }-campaign, skirmish, pvp, coop, defense, screensaver, testCoop, testDefense, peekCampaign, peekSkirmish :: ModeKind--campaign = ModeKind- { msymbol = 'r'- , mname = "campaign"- , mfreq = [("campaign", 1)]- , mplayers = playersCampaign- , mcaves = cavesCampaign- }--skirmish = ModeKind- { msymbol = 's'- , mname = "skirmish"- , mfreq = [("skirmish", 1)]- , mplayers = playersSkirmish- , mcaves = cavesCombat- }--pvp = ModeKind- { msymbol = 'p'- , mname = "PvP"- , mfreq = [("PvP", 1)]- , mplayers = playersPvP- , mcaves = cavesCombat- }--coop = ModeKind- { msymbol = 'c'- , mname = "Coop"- , mfreq = [("Coop", 1)]- , mplayers = playersCoop- , mcaves = cavesCampaign- }--defense = ModeKind- { msymbol = 'd'- , mname = "defense"- , mfreq = [("defense", 1)]- , mplayers = playersDefense- , mcaves = cavesDefense- }--screensaver = ModeKind- { msymbol = 'n'- , mname = "screensaver"- , mfreq = [("screensaver", 1)]- , mplayers = playersScreensaver- , mcaves = cavesScreensaver- }--testCoop = ModeKind- { msymbol = 't'- , mname = "testCoop"- , mfreq = [("testCoop", 1)]- , mplayers = playersTestCoop- , mcaves = cavesScreensaver- }--testDefense = ModeKind- { msymbol = 'u'- , mname = "testDefense"- , mfreq = [("testDefense", 1)]- , mplayers = playersTestDefense- , mcaves = cavesDefense- }--peekCampaign = ModeKind- { msymbol = 'e'- , mname = "peekCampaign"- , mfreq = [("peekCampaign", 1)]- , mplayers = playersPeekCampaign- , mcaves = cavesCampaign- }--peekSkirmish = ModeKind- { msymbol = 'f'- , mname = "peekSkirmish"- , mfreq = [("peekSkirmish", 1)]- , mplayers = playersPeekSkirmish- , mcaves = cavesCombat- }---playersCampaign, playersSkirmish, playersPvP, playersCoop, playersDefense, playersScreensaver, playersTestCoop, playersTestDefense, playersPeekCampaign, playersPeekSkirmish :: Players--playersCampaign = Players- { playersList = [ playerHero {playerInitial = 1}- , playerMonster ]- , playersEnemy = [("Adventuring Party", "Monster Hive")]- , playersAlly = [] }--playersSkirmish = Players- { playersList = [ playerHero {playerName = "White"}- , playerAntiHero {playerName = "Purple"}- , playerHorror ]- , playersEnemy = [ ("White", "Purple")- , ("White", "Horror Den")- , ("Purple", "Horror Den") ]- , playersAlly = [] }--playersPvP = Players- { playersList = [ playerHero {playerName = "Red"}- , playerHero {playerName = "Blue"}- , playerHorror ]- , playersEnemy = [ ("Red", "Blue")- , ("Red", "Horror Den")- , ("Blue", "Horror Den") ]- , playersAlly = [] }--playersCoop = Players- { playersList = [ playerHero { playerName = "Coral"- , playerInitial = 1 }- , playerHero { playerName = "Amber"- , playerInitial = 1 }- , playerMonster ]- , playersEnemy = [ ("Coral", "Monster Hive")- , ("Amber", "Monster Hive") ]- , playersAlly = [("Coral", "Amber")] }--playersDefense = Players- { playersList = [ playerMonster { playerInitial = 1- , playerEntry = toEnum (-1)- , playerAiLeader = False- , playerHuman = True- , playerUI = True }- , playerAntiHero {playerName = "Green"}- , playerAntiHero {playerName = "Yellow"}- , playerAntiHero {playerName = "Cyan"} ]- , playersEnemy = [ ("Green", "Monster Hive")- , ("Yellow", "Monster Hive")- , ("Cyan", "Monster Hive") ]- , playersAlly = [ ("Green", "Yellow")- , ("Green", "Cyan")- , ("Yellow", "Cyan") ] }--playersScreensaver = Players- { playersList = [ playerHero { playerInitial = 5- , playerAiLeader = True- , playerHuman = False }- , playerMonster ]- , playersEnemy = [("Adventuring Party", "Monster Hive")]- , playersAlly = [] }--playersTestCoop = Players- { playersList = [ playerHero { playerName = "Coral"- , playerAiLeader = True- , playerHuman = False }- , playerHero { playerName = "Amber"- , playerAiLeader = True- , playerHuman = False }- , playerMonster ]- , playersEnemy = [ ("Coral", "Monster Hive")- , ("Amber", "Monster Hive") ]- , playersAlly = [("Coral", "Amber")] }--playersTestDefense = Players- { playersList = [ playerMonster { playerInitial = 1- , playerEntry = toEnum (-1)- , playerUI = True }- , playerAntiHero {playerName = "Green"}- , playerAntiHero {playerName = "Yellow"}- , playerAntiHero {playerName = "Cyan"} ]- , playersEnemy = [ ("Green", "Monster Hive")- , ("Yellow", "Monster Hive")- , ("Cyan", "Monster Hive") ]- , playersAlly = [ ("Green", "Yellow")- , ("Green", "Cyan")- , ("Yellow", "Cyan") ] }--playersPeekCampaign = Players- { playersList = [ playerHero {playerInitial = 1}- , playerMonster {playerUI = True} ]- , playersEnemy = [("Adventuring Party", "Monster Hive")]- , playersAlly = [] }--playersPeekSkirmish = Players- { playersList = [ playerHero {playerName = "White"}- , playerAntiHero { playerName = "Purple"- , playerUI = True }- , playerHorror ]- , playersEnemy = [ ("White", "Purple")- , ("White", "Horror Den")- , ("Purple", "Horror Den") ]- , playersAlly = [] }---playerHero, playerAntiHero, playerMonster, playerHorror :: Player--playerHero = Player- { playerName = "Adventuring Party"- , playerFaction = "hero"- , playerEntry = toEnum (-1)- , playerInitial = 3- , playerAiLeader = False- , playerAiOther = True- , playerHuman = True- , playerUI = True- }--playerAntiHero = playerHero- { playerAiLeader = True- , playerHuman = False- , playerUI = False- }--playerMonster = Player- { playerName = "Monster Hive"- , playerFaction = "monster"- , playerEntry = toEnum (-3)- , playerInitial = 5- , playerAiLeader = True- , playerAiOther = True- , playerHuman = False- , playerUI = False- }--playerHorror = Player- { playerName = "Horror Den"- , playerFaction = "horror"- , playerEntry = toEnum (-1)- , playerInitial = 0- , playerAiLeader = True- , playerAiOther = True- , playerHuman = False- , playerUI = False- }---cavesCampaign, cavesCombat, cavesDefense, cavesScreensaver :: Caves--cavesCampaign = EM.fromList [ (toEnum (-1), ("caveRogue", Just True))- , (toEnum (-2), ("caveRogue", Nothing))- , (toEnum (-3), ("caveEmpty", Nothing))- , (toEnum (-10), ("caveNoise", Nothing))]--cavesCombat = EM.fromList [(toEnum (-3), ("caveCombat", Nothing))]--cavesDefense = EM.fromList [ (toEnum (-1), ("dng", Nothing))- , (toEnum (-5), ("caveEmpty", Just False))]--cavesScreensaver = EM.fromList [ (toEnum (-1), ("caveRogue", Nothing))- , (toEnum (-10), ("caveNoise", Just False))]
− LambdaHack/Content/PlaceKind.hs
@@ -1,71 +0,0 @@--- | Rooms, halls and passages for LambdaHack.-module Content.PlaceKind ( cdefs ) where--import Game.LambdaHack.Common.ContentDef-import Game.LambdaHack.Content.PlaceKind--cdefs :: ContentDef PlaceKind-cdefs = ContentDef- { getSymbol = psymbol- , getName = pname- , getFreq = pfreq- , validate = pvalidate- , content =- [rect, pillar, pillarC, pillar3, colonnade, colonnadeW]- }-rect, pillar, pillarC, pillar3, colonnade, colonnadeW :: PlaceKind--rect = PlaceKind -- Valid for any nonempty area, hence low frequency.- { psymbol = 'r'- , pname = "room"- , pfreq = [("rogue", 100)]- , pcover = CStretch- , pfence = FNone- , ptopLeft = [ "--"- , "|."- ]- }-pillar = PlaceKind- { psymbol = 'p'- , pname = "pillar room"- , pfreq = [("rogue", 1000)] -- larger rooms require support pillars- , pcover = CStretch- , pfence = FNone- , ptopLeft = [ "-----"- , "|...."- , "|.O.."- , "|...."- , "|...."- ]- }-pillarC = pillar- { ptopLeft = [ "-----"- , "|O..."- , "|...."- , "|...."- , "|...."- ]- }-pillar3 = pillar- { ptopLeft = [ "-----"- , "|&.O."- , "|...."- , "|O..."- , "|...."- ]- }-colonnade = PlaceKind- { psymbol = 'c'- , pname = "colonnade"- , pfreq = [("rogue", 500)]- , pcover = CAlternate- , pfence = FFloor- , ptopLeft = [ "O."- , ".O"- ]- }-colonnadeW = colonnade- { ptopLeft = [ "O."- , ".."- ]- }
− LambdaHack/Content/RuleKind.hs
@@ -1,82 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--- | Game rules and assorted game setup data for LambdaHack.-module Content.RuleKind ( cdefs ) where--import Language.Haskell.TH.Syntax---- Cabal-import qualified Paths_LambdaHack as Self (getDataFileName, version)--import Game.LambdaHack.Common.ContentDef-import qualified Game.LambdaHack.Common.Feature as F-import Game.LambdaHack.Common.Vector-import Game.LambdaHack.Content.RuleKind-import Game.LambdaHack.Content.TileKind--cdefs :: ContentDef RuleKind-cdefs = ContentDef- { getSymbol = rsymbol- , getName = rname- , getFreq = rfreq- , validate = ruvalidate- , content =- [standard]- }--standard :: RuleKind-standard = RuleKind- { rsymbol = 's'- , rname = "standard LambdaHack ruleset"- , rfreq = [("standard", 100)]- -- Check whether one position is accessible from another.- -- Precondition: the two positions are next to each other.- -- Apart of checking the target tile, we forbid diagonal movement- -- to and from doors.- , raccessible = \ lxsize spos src tpos tgt ->- let getTo F.CloseTo{} = True- getTo _ = False- in F.Walkable `elem` tfeature tgt- && not ((any getTo (tfeature src) ||- any getTo (tfeature tgt))- && diagonal lxsize (displacement spos tpos))- , rtitle = "LambdaHack"- , rpathsDataFile = Self.getDataFileName- , rpathsVersion = Self.version- , ritemMelee = ")"- , ritemRanged = "|"- -- Wasting weapons and armour would be too cruel to the player.- , ritemProject = "!?|/"- -- 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.- , rcfgRulesDefault = $(do- qAddDependentFile "config.rules.default"- x <- qRunIO (readFile "config.rules.default")- lift x)- , rcfgUIDefault = $(do- qAddDependentFile "config.ui.default"- x <- qRunIO (readFile "config.ui.default")- lift x)- -- 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 characters) that is ignored.- -- For a different screen size, the picture is centered and the outermost- -- rows and columns cloned. When displayed in the Main Menu screen,- -- it's overwritten with the game version string and keybinding strings.- -- The game version string begins and ends with a space and is placed- -- in the very bottom right corner. The keybindings overwrite places- -- marked with 25 left curly brace signs '{' in a row. The sign is forbidden- -- everywhere else. A specific number of such places with 25 left braces- -- are required, at most one per row, and all are overwritten- -- with text that is flushed left and padded with spaces.- -- The Main Menu is displayed dull white on black.- -- TODO: Show highlighted keybinding in inverse video or bright white on grey- -- background. The spaces that pad keybindings are not highlighted.- , rmainMenuArt = $(do- qAddDependentFile "MainMenu.ascii"- x <- qRunIO (readFile "MainMenu.ascii")- lift x)- }
− LambdaHack/Content/TileKind.hs
@@ -1,307 +0,0 @@--- | Terrain tiles for LambdaHack.-module Content.TileKind ( cdefs ) where--import Game.LambdaHack.Common.Color-import Game.LambdaHack.Common.ContentDef-import qualified Game.LambdaHack.Common.Effect as Effect-import Game.LambdaHack.Common.Feature-import Game.LambdaHack.Content.TileKind--cdefs :: ContentDef TileKind-cdefs = ContentDef- { getSymbol = tsymbol- , getName = tname- , getFreq = tfreq- , validate = tvalidate- , content =- [wall, hardRock, pillar, pillarCache, tree, wallV, wallSuspectV, doorClosedV, doorOpenV, wallH, wallSuspectH, doorClosedH, doorOpenH, stairsUpDark, stairsUpLit, stairsDark, stairsLit, stairsDownDark, stairsDownLit, escapeUpDark, escapeUpLit, escapeDownDark, escapeDownLit, unknown, floorCorridorLit, floorCorridorDark, floorArenaLit, floorArenaDark, floorItemLit, floorItemDark, floorActorItemLit, floorActorItemDark, floorRedDark, floorBlueDark, floorGreenDark, floorBrownDark, floorRedLit, floorBlueLit, floorGreenLit, floorBrownLit]- }-wall, hardRock, pillar, pillarCache, tree, wallV, wallSuspectV, doorClosedV, doorOpenV, wallH, wallSuspectH, doorClosedH, doorOpenH, stairsUpDark, stairsUpLit, stairsDark, stairsLit, stairsDownDark, stairsDownLit, escapeUpDark, escapeUpLit, escapeDownDark, escapeDownLit, unknown, floorCorridorLit, floorCorridorDark, floorArenaLit, floorArenaDark, floorItemLit, floorItemDark, floorActorItemLit, floorActorItemDark, floorRedDark, floorBlueDark, floorGreenDark, floorBrownDark, floorRedLit, floorBlueLit, floorGreenLit, floorBrownLit :: TileKind--wall = TileKind- { tsymbol = ' '- , tname = "bedrock"- , tfreq = [("fillerWall", 1), ("litLegend", 100), ("darkLegend", 100)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = []- }-hardRock = TileKind- { tsymbol = ' '- , tname = "impenetrable bedrock"- , tfreq = [("outer fence", 1)]- , tcolor = BrBlack- , tcolor2 = BrBlack- , tfeature = [Impenetrable]- }-pillar = TileKind- { tsymbol = 'O'- , tname = "rock"- , tfreq = [ ("cachable", 70)- , ("litLegend", 100), ("darkLegend", 100)- , ("noiseSet", 55), ("combatSet", 3) ]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = []- }-pillarCache = TileKind- { tsymbol = '&'- , tname = "cache"- , tfreq = [ ("cachable", 30)- , ("litLegend", 100), ("darkLegend", 100) ]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = [Cause $ Effect.CreateItem 1, ChangeTo "cachable"]- }-tree = TileKind- { tsymbol = 'O'- , tname = "tree"- , tfreq = [("combatSet", 8)]- , tcolor = BrGreen- , tcolor2 = Green- , tfeature = []- }-wallV = TileKind- { tsymbol = '|'- , tname = "granite wall"- , tfreq = [("litLegend", 100), ("darkLegend", 100)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = [HideAs "suspect vertical wall"]- }-wallSuspectV = TileKind- { tsymbol = '|'- , tname = "moldy wall"- , tfreq = [("suspect vertical wall", 1)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = [ Suspect- , RevealAs "vertical closed door"- ]- }-doorClosedV = TileKind- { tsymbol = '+'- , tname = "closed door"- , tfreq = [("vertical closed door", 1)]- , tcolor = Brown- , tcolor2 = BrBlack- , tfeature = [ Exit- , OpenTo "vertical open door"- , HideAs "suspect vertical wall"- ]- }-doorOpenV = TileKind- { tsymbol = '-'- , tname = "open door"- , tfreq = [("vertical open door", 1)]- , tcolor = Brown- , tcolor2 = BrBlack- , tfeature = [ Walkable, Clear, Exit- , CloseTo "vertical closed door"- ]- }-wallH = TileKind- { tsymbol = '-'- , tname = "granite wall"- , tfreq = [("litLegend", 100), ("darkLegend", 100)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = [HideAs "suspect horizontal wall"]- }-wallSuspectH = TileKind- { tsymbol = '-'- , tname = "scratched wall"- , tfreq = [("suspect horizontal wall", 1)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = [ Suspect- , RevealAs "horizontal closed door"- ]- }-doorClosedH = TileKind- { tsymbol = '+'- , tname = "closed door"- , tfreq = [("horizontal closed door", 1)]- , tcolor = Brown- , tcolor2 = BrBlack- , tfeature = [ Exit- , OpenTo "horizontal open door"- , HideAs "suspect horizontal wall"- ]- }-doorOpenH = TileKind- { tsymbol = '|'- , tname = "open door"- , tfreq = [("horizontal open door", 1)]- , tcolor = Brown- , tcolor2 = BrBlack- , tfeature = [ Walkable, Clear, Exit- , CloseTo "horizontal closed door"- ]- }-stairsUpDark = TileKind- { tsymbol = '<'- , tname = "staircase up"- , tfreq = [("darkLegend", 100)]--- Disabled, because the yellow artificial light does not fit LambdaHack.--- , tcolor = BrYellow--- Dark room interior, OTOH, is fine:- , tcolor = BrWhite- , tcolor2 = BrBlack- , tfeature = [Walkable, Clear, Exit, Cause $ Effect.Ascend 1]- }-stairsUpLit = stairsUpDark- { tfreq = [("litLegend", 100)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = Lit : tfeature stairsUpDark- }-stairsDark = TileKind- { tsymbol = '>'- , tname = "staircase"- , tfreq = [("darkLegend", 100)]- , tcolor = BrCyan- , tcolor2 = Cyan -- TODO- , tfeature = [ Walkable, Clear, Exit- , Cause $ Effect.Ascend 1- , Cause $ Effect.Ascend (-1) ]- }-stairsLit = stairsDark- { tfreq = [("litLegend", 100)]- , tcolor = BrCyan- , tcolor2 = Cyan -- TODO- , tfeature = Lit : tfeature stairsDark- }-stairsDownDark = TileKind- { tsymbol = '>'- , tname = "staircase down"- , tfreq = [("darkLegend", 100)]- , tcolor = BrWhite- , tcolor2 = BrBlack- , tfeature = [Walkable, Clear, Exit, Cause $ Effect.Ascend (-1)]- }-stairsDownLit = stairsDownDark- { tfreq = [("litLegend", 100)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = Lit : tfeature stairsDownDark- }-escapeUpDark = TileKind- { tsymbol = '<'- , tname = "exit trapdoor up"- , tfreq = [("darkLegend", 100)]- , tcolor = BrYellow- , tcolor2 = BrYellow- , tfeature = [Walkable, Clear, Exit, Cause Effect.Escape]- }-escapeUpLit = escapeUpDark- { tfreq = [("litLegend", 100)]- , tfeature = Lit : tfeature escapeUpDark- }-escapeDownDark = TileKind- { tsymbol = '>'- , tname = "exit trapdoor down"- , tfreq = [("darkLegend", 100)]- , tcolor = BrYellow- , tcolor2 = BrYellow- , tfeature = [Walkable, Clear, Exit, Cause Effect.Escape]- }-escapeDownLit = escapeDownDark- { tfreq = [("litLegend", 100)]- , tfeature = Lit : tfeature escapeDownDark- }-unknown = TileKind- { tsymbol = ' '- , tname = "unknown space"- , tfreq = [("unknown space", 1)]- , tcolor = defFG- , tcolor2 = BrWhite- , tfeature = []- }-floorCorridorLit = TileKind- { tsymbol = '#'- , tname = "corridor"- , tfreq = [("floorCorridorLit", 1)]- , tcolor = BrWhite- , tcolor2 = defFG- , tfeature = [Walkable, Clear, Lit]- }-floorCorridorDark = floorCorridorLit- { tfreq = [("floorCorridorDark", 1)]--- Disabled, because dark corridors and yellow light does not fit LambdaHack.--- , tcolor = BrYellow--- , tcolor2 = BrBlack- , tfeature = [Walkable, Clear]- }-floorArenaLit = floorCorridorLit- { tsymbol = '.'- , tname = "stone floor"- , tfreq = [ ("floorArenaLit", 1)- , ("arenaSet", 1), ("noiseSet", 100), ("combatSet", 100) ]- }-floorArenaDark = floorCorridorDark- { tsymbol = '.'- , tname = "stone floor"- , tfreq = [("arenaSet", 1), ("noiseSet", 100), ("combatSet", 100)]--- Disabled, because the yellow artificial light does not fit LambdaHack.--- , tcolor = BrYellow--- Dark room interior, OTOH, is fine:- , tcolor2 = BrBlack- }-floorItemLit = floorArenaLit- { tfreq = []- , tfeature = CanItem : tfeature floorArenaLit- }-floorItemDark = floorArenaDark- { tfreq = []- , tfeature = CanItem : tfeature floorArenaDark- }-floorActorItemLit = floorItemLit- { tfreq = [("litLegend", 100), ("emptySet", 1)]- , tfeature = CanActor : tfeature floorItemLit- }-floorActorItemDark = floorItemDark- { tfreq = [("darkLegend", 100), ("emptySet", 1)]- , tfeature = CanActor : tfeature floorItemDark- }-floorRedDark = floorArenaDark- { tname = "brick pavement"- , tfreq = [("pathDark", 30)]- , tcolor = BrRed- , tcolor2 = Red- , tfeature = Path : tfeature floorArenaDark- }-floorRedLit = floorRedDark- { tfreq = [("pathLit", 30)]- , tfeature = Lit : tfeature floorRedDark- }-floorBlueDark = floorRedDark- { tname = "granite cobblestones"- , tfreq = [("pathDark", 100)]- , tcolor = BrBlue- , tcolor2 = Blue- }-floorBlueLit = floorBlueDark- { tfreq = [("pathLit", 100)]- , tfeature = Lit : tfeature floorBlueDark- }-floorGreenDark = floorRedDark- { tname = "mossy stone path"- , tfreq = [("pathDark", 100)]- , tcolor = BrGreen- , tcolor2 = Green- }-floorGreenLit = floorGreenDark- { tfreq = [("pathLit", 100)]- , tfeature = Lit : tfeature floorGreenDark- }-floorBrownDark = floorRedDark- { tname = "rotting mahogany deck"- , tfreq = [("pathDark", 10)]- , tcolor = BrMagenta- , tcolor2 = Magenta- }-floorBrownLit = floorBrownDark- { tfreq = [("pathLit", 10)]- , tfeature = Lit : tfeature floorBrownDark- }
− LambdaHack/Main.hs
@@ -1,50 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}--- | The main source code file of LambdaHack. Here the knot of engine--- code pieces and the LambdaHack-specific content defintions is tied,--- resulting in an executable game.-module Main ( main ) where--import qualified Content.ActorKind-import qualified Content.CaveKind-import qualified Content.FactionKind-import qualified Content.ItemKind-import qualified Content.ModeKind-import qualified Content.PlaceKind-import qualified Content.RuleKind-import qualified Content.TileKind-import Game.LambdaHack.Client-import Game.LambdaHack.Client.Action.ActionType-import Game.LambdaHack.Common.Action (MonadAtomic (..))-import Game.LambdaHack.Common.AtomicCmd-import Game.LambdaHack.Common.AtomicSem-import qualified Game.LambdaHack.Common.Kind as Kind-import Game.LambdaHack.Server-import Game.LambdaHack.Server.Action.ActionType-import Game.LambdaHack.Server.AtomicSemSer---- | The game-state semantics of atomic game commands--- as computed on the server.-instance MonadAtomic ActionSer where- execAtomic = atomicSendSem---- | The game-state semantics of atomic game commands--- as computed on clients. Special effects (@SfxAtomic@) don't modify state.-instance MonadAtomic (ActionCli c d) where- execAtomic (CmdAtomic cmd) = cmdAtomicSem cmd- execAtomic (SfxAtomic _) = return ()---- | Tie the LambdaHack engine clients and server code--- with the LambdaHack-specific content defintions and run the game.-main :: IO ()-main =- let copsSlow = Kind.COps- { coactor = Kind.createOps Content.ActorKind.cdefs- , cocave = Kind.createOps Content.CaveKind.cdefs- , cofaction = Kind.createOps Content.FactionKind.cdefs- , coitem = Kind.createOps Content.ItemKind.cdefs- , comode = Kind.createOps Content.ModeKind.cdefs- , coplace = Kind.createOps Content.PlaceKind.cdefs- , corule = Kind.createOps Content.RuleKind.cdefs- , cotile = Kind.createOps Content.TileKind.cdefs- }- in mainSer copsSlow executorSer $ exeFrontend executorCli executorCli
− MainMenu.ascii
@@ -1,26 +0,0 @@------------------------------------------------------------------------------------| |-| >> LambdaHack << |-| |-| |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| {{{{{{{{{{{{{{{{{{{{{{{{{ |-| |-| |-| |-| Version X.X.X (frontend: gtk, engine: LambdaHack X.X.X) |-----------------------------------------------------------------------------------
Makefile view
@@ -12,134 +12,228 @@ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer xcpeekCampaign:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekCampaign --gameMode peekCampaign+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekCampaign --dumpInitRngs --gameMode peekCampaign xcpeekSkirmish:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekSkirmish --gameMode peekSkirmish+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekSkirmish --dumpInitRngs --gameMode peekSkirmish xcfrontendCampaign:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 45 --savePrefix test --gameMode screensaver+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testCampaign --difficulty 1 +xcfrontendSkirmish:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testSkirmish++xcfrontendBattle:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testBattle --difficulty 1++xcfrontendPvP:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testPvP --fovMode Shadow+ xcfrontendCoop:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 180 --fovMode Permissive --savePrefix test --gameMode testCoop+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testCoop --difficulty 1 --fovMode Permissive xcfrontendDefense:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 45 --savePrefix test --gameMode testDefense+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testDefense --difficulty 9 +xcbenchCampaign:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendNo --benchmark --stopAfter 60 --gameMode testCampaign --difficulty 1 --setDungeonRng 42 --setMainRng 42 -xctest-travis: xctest-short xctest-medium+xcbenchBattle:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendNo --benchmark --stopAfter 60 --gameMode testBattle --difficulty 1 --setDungeonRng 42 --setMainRng 42 +xcbenchFrontendCampaign:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 100000 --savePrefix test --benchmark --stopAfter 60 --gameMode testCampaign --difficulty 1 --setDungeonRng 42 --setMainRng 42++xcbenchFrontendBattle:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 100000 --savePrefix test --benchmark --stopAfter 60 --gameMode testBattle --difficulty 1 --setDungeonRng 42 --setMainRng 42++xcbenchNo: xcbenchCampaign xcbenchBattle++xcbench: xcbenchCampaign xcbenchFrontendCampaign xcbenchBattle xcbenchFrontendBattle++xctest-travis: xctest-short xctest-medium xcbenchNo++xctest-travis-long: xctest-short xctest-long xcbenchNo+ xctest: xctest-short xctest-medium xctest-long xctest-short: xctest-short-new xctest-short-load -xctest-medium: xctestCampaign-medium xctestCoop-medium xctestDefense-medium+xctest-medium: xctestCampaign-medium xctestSkirmish-medium xctestBattle-medium xctestPvP-medium xctestCoop-medium xctestDefense-medium xctest-long: xctestCampaign-long xctestCoop-long xctestDefense-long xctestCampaign-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --gameMode screensaver --frontendStd --stopAfter 1000 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testCampaign --difficulty 1 > /tmp/stdtest.log xctestCampaign-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --gameMode screensaver --frontendStd --dumpConfig --stopAfter 200 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testCampaign --difficulty 1 > /tmp/stdtest.log +xctestSkirmish-long:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testSkirmish > /tmp/stdtest.log++xctestSkirmish-medium:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testSkirmish > /tmp/stdtest.log++xctestBattle-long:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testBattle --difficulty 1 > /tmp/stdtest.log++xctestBattle-medium:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testBattle --difficulty 1 > /tmp/stdtest.log++xctestPvP-long:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testPvP > /tmp/stdtest.log++xctestPvP-medium:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testPvP > /tmp/stdtest.log+ xctestCoop-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --fovMode Permissive --savePrefix test --gameMode testCoop --frontendStd --stopAfter 1000 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testCoop --difficulty 1 > /tmp/stdtest.log xctestCoop-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --fovMode Shadow --savePrefix test --gameMode testCoop --frontendStd --dumpConfig --stopAfter 200 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testCoop --difficulty 1 > /tmp/stdtest.log xctestDefense-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noAnim --maxFps 100000 --savePrefix test --gameMode testDefense --frontendStd --stopAfter 1000 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testDefense --difficulty 9 > /tmp/stdtest.log xctestDefense-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --maxFps 100000 --savePrefix test --gameMode testDefense --frontendStd --dumpConfig --stopAfter 200 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testDefense --difficulty 9 > /tmp/stdtest.log xctest-short-new:- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix campaign --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix skirmish --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix PvP --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix Coop --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix defense --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix peekCampaign --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix peekSkirmish --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix campaign --dumpInitRngs --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix skirmish --dumpInitRngs --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix battle --dumpInitRngs --gameMode battle --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix PvP --dumpInitRngs --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix Coop --dumpInitRngs --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix defense --dumpInitRngs --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix peekCampaign --dumpInitRngs --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix peekSkirmish --dumpInitRngs --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log xctest-short-load:- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix campaign --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix skirmish --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix PvP --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix Coop --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix defense --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekCampaign --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekSkirmish --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix campaign --dumpInitRngs --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix skirmish --dumpInitRngs --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix battle --dumpInitRngs --gameMode battle --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix PvP --dumpInitRngs --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix Coop --dumpInitRngs --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix defense --dumpInitRngs --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekCampaign --dumpInitRngs --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix peekSkirmish --dumpInitRngs --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log play: dist/build/LambdaHack/LambdaHack --dbgMsgSer peekCampaign:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekCampaign --gameMode peekCampaign+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekCampaign --dumpInitRngs --gameMode peekCampaign peekSkirmish:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekSkirmish --gameMode peekSkirmish+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekSkirmish --dumpInitRngs --gameMode peekSkirmish frontendCampaign:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 45 --savePrefix test --gameMode screensaver+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testCampaign --difficulty 1 +frontendSkirmish:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testSkirmish++frontendBattle:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testBattle --difficulty 1++frontendPvP:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testPvP --fovMode Shadow+ frontendCoop:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 180 --fovMode Permissive --savePrefix test --gameMode testCoop+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testCoop --difficulty 1 --fovMode Permissive frontendDefense:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 45 --savePrefix test --gameMode testDefense+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 180 --savePrefix test --dumpInitRngs --gameMode testDefense --difficulty 9 +benchCampaign:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendNo --benchmark --stopAfter 60 --gameMode testCampaign --difficulty 1 --setDungeonRng 42 --setMainRng 42 -test-travis: test-short test-medium+benchBattle:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendNo --benchmark --stopAfter 60 --gameMode testBattle --difficulty 1 --setDungeonRng 42 --setMainRng 42 +benchFrontendCampaign:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 100000 --savePrefix test --benchmark --stopAfter 60 --gameMode testCampaign --difficulty 1 --setDungeonRng 42 --setMainRng 42++benchFrontendBattle:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 100000 --savePrefix test --benchmark --stopAfter 60 --gameMode testBattle --difficulty 1 --setDungeonRng 42 --setMainRng 42++benchNo: benchCampaign benchBattle++bench: benchCampaign benchFrontendCampaign benchBattle benchFrontendBattle+++test-travis: test-short test-medium benchNo++test-travis-long: test-short test-long benchNo+ test: test-short test-medium test-long test-short: test-short-new test-short-load -test-medium: testCampaign-medium testCoop-medium testDefense-medium+test-medium: testCampaign-medium testSkirmish-medium testBattle-medium testPvP-medium testCoop-medium testDefense-medium test-long: testCampaign-long testCoop-long testDefense-long testCampaign-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --gameMode screensaver --frontendStd --stopAfter 1000 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testCampaign --difficulty 1 > /tmp/stdtest.log testCampaign-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --gameMode screensaver --frontendStd --dumpConfig --stopAfter 200 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testCampaign --difficulty 1 > /tmp/stdtest.log +testSkirmish-long:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testSkirmish > /tmp/stdtest.log++testSkirmish-medium:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testSkirmish > /tmp/stdtest.log++testBattle-long:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testBattle --difficulty 1 > /tmp/stdtest.log++testBattle-medium:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testBattle --difficulty 1 > /tmp/stdtest.log++testPvP-long:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testPvP > /tmp/stdtest.log++testPvP-medium:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testPvP > /tmp/stdtest.log+ testCoop-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --fovMode Permissive --savePrefix test --gameMode testCoop --frontendStd --stopAfter 1000 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testCoop --difficulty 1 > /tmp/stdtest.log testCoop-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --fovMode Shadow --savePrefix test --gameMode testCoop --frontendStd --dumpConfig --stopAfter 200 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testCoop --difficulty 1 > /tmp/stdtest.log testDefense-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noAnim --maxFps 100000 --savePrefix test --gameMode testDefense --frontendStd --stopAfter 1000 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 580 --dumpInitRngs --gameMode testDefense --difficulty 9 > /tmp/stdtest.log testDefense-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --maxFps 100000 --savePrefix test --gameMode testDefense --frontendStd --dumpConfig --stopAfter 200 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --noMore --noDelay --noAnim --maxFps 100000 --savePrefix test --frontendStd --benchmark --stopAfter 280 --dumpInitRngs --gameMode testDefense --difficulty 9 > /tmp/stdtest.log test-short-new:- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix campaign --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix skirmish --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix PvP --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix Coop --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix defense --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix peekCampaign --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix peekSkirmish --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix campaign --dumpInitRngs --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix skirmish --dumpInitRngs --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix battle --dumpInitRngs --gameMode battle --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix PvP --dumpInitRngs --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix Coop --dumpInitRngs --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix defense --dumpInitRngs --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix peekCampaign --dumpInitRngs --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix peekSkirmish --dumpInitRngs --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log test-short-load:- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix campaign --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix skirmish --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix PvP --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix Coop --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix defense --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekCampaign --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log- while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekSkirmish --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix campaign --dumpInitRngs --gameMode campaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix skirmish --dumpInitRngs --gameMode skirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix battle --dumpInitRngs --gameMode battle --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix PvP --dumpInitRngs --gameMode PvP --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix Coop --dumpInitRngs --gameMode Coop --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix defense --dumpInitRngs --gameMode defense --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekCampaign --dumpInitRngs --gameMode peekCampaign --frontendStd --stopAfter 0 > /tmp/stdtest.log+ while true; do echo ' '; echo '.'; sleep 1; done | dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix peekSkirmish --dumpInitRngs --gameMode peekSkirmish --frontendStd --stopAfter 0 > /tmp/stdtest.log + # The rest of the makefile is unmaintained at the moment. default : dist/setup-config@@ -161,4 +255,4 @@ runghc Setup clean ghci :- ghci -XCPP -idist/build/autogen:Game/LambdaHack+ ghci -XCPP -idist/build/autogen:.
− PLAYING.md
@@ -1,171 +0,0 @@-Playing LambdaHack-==================--LambdaHack is a small dungeon crawler illustrating the roguelike game engine-library also called LambdaHack. Playing the game involves walking around-the dungeon, alone or in a party of fearless adventurers, setting up ambushes,-hiding in shadow, covering tracks, breaking through to deeper caves,-bumping into monsters, doors and walls, gathering magical treasure-and making creative use of it. The bloodthirsty monsters do the same,-intelligence allowing, while tirelessly chasing the elusive heroes-by smell and sight.--Once the few basic command keys and on-screen symbols are learned,-mastery and enjoyment of the game is the matter of tactical skill-and literary imagination. To be honest, a lot of imagination is required-for this simple game, but it's playable and winnable.-The game also features multiplayer cooperative and competitive modes,-though only a shared-screen interface is provided at this time.---Dungeon----------The heroes are marked on the map with symbols '@' and '1' through '9'.-Their goal is to explore the dungeon, battle the horrors within,-gather as much gold and gems as possible, and escape to tell the tale.-The dungeon of the campaign mode game consists of 10 levels and each level-consists of large number of tiles. The basic tiles are as follows.-- dungeon terrain type on-screen symbol- floor .- corridor #- wall (horizontal and vertical) - and |- rock or tree O- cache &- stairs up <- stairs down >- open door | and -- closed door +- bedrock blank--The game world is persistent, i.e., every time the player visits a level-during a single game, the level layout is the same.---Keys-------You move throughout the level using the numerical keypad or-the vi text editor keys (also known as "Rogue-like keys").-- 7 8 9 y k u- \|/ \|/- 4-5-6 h-.-l- /|\ /|\- 1 2 3 b j n--SHIFT (or CTRL) and a movement key make the selected hero run in the indicated-direction, until anything of interest is spotted. The '5' and '.' keys take-a turn to brace for combat, which gives a chance to block blows-for the remainder of the turn. Melee, searching for secret doors-and opening closed doors can be done by bumping into a monster,-a wall and a door, respectively.--Below are the default keys for major commands. Those of them that take-hero time are marked with a star.-- key command- < ascend a level*- > descend a level*- ? display help- I display inventory- c close a door*- d drop an object*- g get an object*- o open a door*- q quaff a potion*- r read a scroll*- t throw a dart*- z zap a wand*--To make a ranged attack, you need to set your target first,-using targeting mode ('*' and '/' keys). The target, for the few-commands that require any, is indicated by the targeting cursor.-To avoid confusion, commands that take time are blocked when targeting-at a remote level (when the cursor is displayed on a different level-than the selected hero). The targeting commands and all the less used-commands are listed below. None of them takes hero time.-- key command- ESC cancel action or bring up the Main Menu- RET accept choice- TAB cycle among heroes on the level- SHIFT-TAB cycle among heroes in the dungeon- * target monster- + swerve targeting line- - unswerve targeting line- / target position- P display previous messages- S mark smell- T mark suspect terrain- V mark visible area- [ target next shallower level- ] target next deeper level- { target 10 levels shallower- } target 10 levels deeper- 0--9 select a hero anywhere in the dungeon--Commands for saving and exiting the current game, starting a new game, etc.,-are listed in the Main Menu, brough up by the ESC key.-Some of the game modes are multiplayer or feature multiple computer-players (allied or not). The setup of the modes can be modified-via a configuration file.-- key command- CTRL-x save and exit- CTRL-r new campaign game- CTRL-k new skirmish game- CTRL-p new PvP game- CTRL-o new Coop game- CTRL-e new defense game--There are also some debug, testing and cheat options and game modes-that can be specified on the command line when starting the game server.-Use at your own peril! :) Of these, you may find the screensaver modes-the least spoilery and the most fun, e.g.:-- LambdaHack --newGame --noMore --maxFps 45 --savePrefix screensaver --gameMode screensaver---Monsters-----------Heroes are not alone in the dungeon. Monsters roam the dark caves-and crawl from damp holes day and night. While heroes pay attention-to all other party members and take care to move one at a time,-monsters don't care about each other and all move at once,-sometimes brutally colliding by accident.--When the hero bumps into a monster or a monster attacks the hero,-melee combat occurs. The best weapon carried by each opponent-is taken into account for calculating bonus damage. The total damage-the current hero can potentially inflict is displayed at the bottom-of the screen. The total damage potential of a monster may change-as it finds and picks up new weapons. Heroes and monsters running into-one another (with the Shift key) do not inflict damage, but change places.-This gives the opponent a free blow, but can improve the tactical situation-or aid escape.--Throwing weapons at targets wounds them, consuming the weapon in the process.-Target a monster with the '*' key from the top keyboard row or from keypad.-You may throw any object in your possession (press '?' to choose-an object and press it again for a non-standard choice) or on the floor-(press '-'). Only objects of a few kinds inflict any damage.-Whenever the monster's or hero's hit points reach zero, the combatant dies.-When the last hero dies, the game ends.---On Winning and Dying-----------------------You win the game if you escape the dungeon alive (or eliminate-all opposition, in some game modes). Your score is-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).--If all heroes die, your score is halved and only the treasure carried-by the last standing hero counts. You are free to start again-from a different entrance to the dungeon, but all your previous wealth-is gone and fresh, undaunted enemies bar your way.
README.md view
@@ -1,4 +1,4 @@-LambdaHack [](http://travis-ci.org/kosmikus/LambdaHack)+LambdaHack [](http://travis-ci.org/kosmikus/LambdaHack)[](https://drone.io/github.com/kosmikus/LambdaHack/latest) ========== This is an alpha release of LambdaHack, a [Haskell] [1] game engine@@ -13,11 +13,10 @@ in the strict and type-safe separation of code and content and of clients (human and AI-controlled) and server. Long-term goals for LambdaHack include support for multiplayer tactical squad combat, in-game content creation,-auto-balancing and persistent content modification based-on player behaviour.+auto-balancing and persistent content modification based on player behaviour. The engine comes with a sample code for a little dungeon crawler,-called LambdaHack and described in PLAYING.md. The engine and the example+called LambdaHack and described in `PLAYING.md`. The engine and the example game are bundled together in a single [Hackage] [3] package. You are welcome to create your own games by modifying the sample game and the engine code, but please consider eventually splitting your changes@@ -32,9 +31,11 @@ Compilation and installation ---------------------------- -The library is best compiled and installed via Cabal, which also takes care-of all dependencies. The latest official version of the library-can be downloaded automatically by Cabal from [Hackage] [3] as follows+The library is best compiled and installed via Cabal (already a part+of your OS distribution, or available within [The Haskell Platform] [7]),+which also takes care of all the dependencies. The latest official+version of the library can be downloaded automatically by Cabal+from [Hackage] [3] as follows cabal install LambdaHack @@ -53,33 +54,31 @@ Compatibility notes ------------------- -The current code was tested with GHC 7.6.3, but should also work with-other GHC versions (see file .travis.yml.7.4.2 for GHC 7.4.2 commands).+The current code was tested with GHC 7.6 and 7.8, but should also work with+other GHC versions (see file `.travis.yml.7.4.2` for GHC 7.4 commands). -If you are using the curses or vty frontends,-numerical keypad may not work correctly depending on the versions-of curses, terminfo and terminal emulators.-Selecting heroes via number keys or SHIFT-keypad keys is disabled-with curses, because CTRL-keypad for running does not work there,-so the numbers produced by the keypad have to be used. With vty on xterm,-CTRL-direction keys seem to work OK, but on rxvt they do not.-Vi keys (ykuhlbjn) should work everywhere regardless. Gtk works fine, too.+If you are using the terminal frontends, numerical keypad may not work+correctly depending on versions of the libraries, terminfo and terminal+emulators. The curses frontend is not fully supported due to the limitations+of the curses library. With the vty frontend run in an xterm,+CTRL-keypad keys for running seem to work OK, but on rxvt they do not.+Vi keys (ykuhlbjn) should work everywhere regardless. GTK works fine, too. Testing and debugging --------------------- -The Makefile contains many sample test commands. All that use the screensaver+The `Makefile` contains many sample test commands. All that use the screensaver game modes (AI vs. AI) and the simplest stdout frontend are gathered-in `make test`. Of these, travis runs the set contained in-`make test-travis` on each push to the repo. Commands with prefix+in `make test`. Of these, travis runs one of the sets prefixed+`test-travis` on each push to the repo. Commands with prefix `frontend` run AI vs. AI games with the standard, user-friendly frontend. Commands with prefix `peek` set up a game mode where the player peeks into AI moves each time an AI actor dies or autosave kicks in. Run `LambdaHack --help` to see a brief description of all debug options. Of these, `--sniffIn` and `--sniffOut` are very useful (though verbose and initially cryptic), for monitoring the traffic between clients-and the server. Some options in config files may turn out useful too,+and the server. Some options in the config file may turn out useful too, though they mostly overlap with commandline options (and will be totally merged at some point). @@ -101,7 +100,7 @@ ------------------- For more information, visit the [wiki] [4]-and see the files PLAYING.md, CREDITS and LICENSE.+and see `GameDefinition/PLAYING.md`, `CREDITS` and `LICENSE`. Have fun! @@ -113,3 +112,4 @@ [4]: https://github.com/kosmikus/LambdaHack/wiki [5]: http://github.com/kosmikus/LambdaHack [6]: http://hackage.haskell.org/package/Allure+[7]: http://www.haskell.org/platform
changelog view
@@ -1,44 +1,55 @@-0.2.0-- * the LambdaHack engine becomes a Haskell library-- * the LambdaHack game depends on the engine library--0.2.1-- * missiles flying for three turns (by an old kosmikus' idea)-- * visual feedback for targeting-- * animations of combat and individual monster moves--0.2.6-- * the Main Menu+v0.2.12 - * improved and configurable mode of squad combat+ * improve and simplify dungeon generation+ * simplify running and permit multi-actor runs+ * let items explode and generate shrapnel projectiles+ * add game difficulty setting (initial HP scaling right now)+ * allow recording, playing back and looping commands+ * implement pathfinding via per-actor BFS over the whole level+ * extend setting targets for actors in UI tremendously+ * implement autoexplore, go-to-target, etc., as macros+ * let AI use pathfinding, switch leaders, pick levels to swarm to+ * force level/leader changes on spawners (even when played by humans)+ * extend and redesign UI bottom status lines+ * get rid of CPS style monads, aborts and WriterT+ * benchmark and optimize the code, in particular using Data.Vector+ * split off and use the external library assert-failure+ * simplify config files and limit the number of external dependencies -0.2.6.5+v0.2.10 - * this is a minor release, primarily intended to fix the broken haddock documentation on Hackage+ * screensaver game modes (AI vs AI)+ * improved AI (can now climbs stairs, etc.)+ * multiple, multi-floor staircases+ * multiple savefiles+ * configurable framerate and combat animations - * 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+v0.2.8 -0.2.8+ * cooperative and competitive multiplayer (shared-screen only in this version)+ * overhauled searching+ * rewritten engine code to have a single server that sends restricted game state updates to many fat clients, while a thin frontend layer multiplexes visuals from a subset of the clients - * cooperative and competitive multiplayer (shared-screen only in this version)- * overhauled searching+v0.2.6.5 - * rewritten engine code to have a single server that sends restricted game state updates to many fat clients, while a thin frontend layer multiplexes visuals from a subset of the clients+ * this is a minor release, primarily intended to fix the broken haddock documentation on Hackage+ * 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 -0.2.10+v0.2.6 - * screensaver game modes (AI vs AI)+ * the Main Menu+ * improved and configurable mode of squad combat - * improved AI (can now climbs stairs, etc.)+v0.2.1 - * multiple, multi-floor staircases+ * missiles flying for three turns (by an old kosmikus' idea)+ * visual feedback for targeting+ * animations of combat and individual monster moves - * multiple savefiles+v0.2.0 - * configurable framerate and combat animations+ * the LambdaHack engine becomes a Haskell library+ * the LambdaHack game depends on the engine library
− config.rules.default
@@ -1,28 +0,0 @@-; ; 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.--; [engine]-; ;dungeonRandomGenerator: 42-; firstDeathEnds: False-; fovMode: Digital 12-; ;fovMode: Permissive-; ;fovMode: Shadow-; ;startingRandomGenerator: 42-; saveBkpClips: 500--; [file]-; ; Names (or prefixes) of various game files.-; ; They reside in ~/.LambdaHack or similar.-; savePrefix: save-; scoresFile: scores--; [heroName]-; HeroName_0: Haskell Alvin-; HeroName_1: Alonzo Barkley-; HeroName_2: Ines Galenti-; HeroName_3: Ernst Abraham-; HeroName_4: Samuel Saunders-; HeroName_5: Roger Robin
− config.ui.default
@@ -1,63 +0,0 @@-; ; 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.-; ;-; KP_Begin: Wait-; g: Pickup-; d: Drop-; t: Project [ApplyItem {verb = "throw", object = "missile", symbol = '|'}]-; z: Project [ApplyItem {verb = "zap", object = "wand", symbol = '/'}]-; q: Apply [ApplyItem {verb = "quaff", object = "potion", symbol = '!'}]-; r: Apply [ApplyItem {verb = "read", object = "scroll", symbol = '?'}]-; c: AlterDir [AlterFeature {verb = "close", object = "door", feature = CloseTo "vertical closed door"}, AlterFeature {verb = "close", object = "door", feature = CloseTo "horizontal closed door"}]-; o: AlterDir [AlterFeature {verb = "open", object = "door", feature = OpenTo "vertical open door"}, AlterFeature {verb = "open", object = "door", feature = OpenTo "horizontal open door"}]-; less: TriggerTile [TriggerFeature {verb = "ascend", object = "level", feature = Cause (Ascend 1)}, TriggerFeature {verb = "escape", object = "dungeon", feature = Cause Escape}]-; greater: TriggerTile [TriggerFeature {verb = "descend", object = "level", feature = Cause (Ascend (-1))}, TriggerFeature {verb = "escape", object = "dungeon", feature = Cause Escape}]-; CTRL-r: GameRestart "campaign"-; ; TODO: pick game modes via arrow keys, not special keys-; CTRL-k: GameRestart "skirmish"-; CTRL-p: GameRestart "PvP"-; CTRL-o: GameRestart "Coop"-; CTRL-e: GameRestart "defense"-; CTRL-x: GameExit-; CTRL-s: GameSave-; Tab: MemberCycle-; ISO_Left_Tab: MemberBack-; I: Inventory-; slash: TgtFloor-; asterisk: TgtEnemy-; bracketleft: TgtAscend 1-; bracketright: TgtAscend (-1)-; braceleft: TgtAscend 10-; braceright: TgtAscend (-10)-; plus: EpsIncr True-; minus: EpsIncr False-; Escape: Cancel-; Return: Accept-; space: Clear-; P: History-; V: MarkVision-; S: MarkSmell-; T: MarkSuspect-; question: Help--; [file]-; ; Names (or prefixes) of various game files.-; ; They reside in ~/.LambdaHack or similar.-; savePrefix: save--; [macros]-; ; Handy with Vi keys:-; comma: g-; period: KP_Begin--; [ui]-; font: Terminus,Monospace normal normal normal normal 12-; historyMax: 5000-; maxFps: 15-; noAnim: False
− scores
binary file changed (299 → absent bytes)