LambdaHack 0.2.6.5 → 0.2.8
raw patch · 168 files changed
+15119/−10702 lines, 168 filesdep +deepseqdep +enummapsetdep +ghc-primdep ~binarydep ~containersdep ~miniutterPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: deepseq, enummapset, ghc-prim, hashable, keys, stm, transformers, unordered-containers
Dependency ranges changed: binary, containers, miniutter
API changes (from Hackage documentation)
- Game.LambdaHack.Ability: Chase :: Ability
- Game.LambdaHack.Ability: Flee :: Ability
- Game.LambdaHack.Ability: Heal :: Ability
- Game.LambdaHack.Ability: Melee :: Ability
- Game.LambdaHack.Ability: Pickup :: Ability
- Game.LambdaHack.Ability: Ranged :: Ability
- Game.LambdaHack.Ability: Tools :: Ability
- Game.LambdaHack.Ability: Track :: Ability
- Game.LambdaHack.Ability: Wander :: Ability
- Game.LambdaHack.Ability: data Ability
- Game.LambdaHack.Ability: instance Bounded Ability
- Game.LambdaHack.Ability: instance Enum Ability
- Game.LambdaHack.Ability: instance Eq Ability
- Game.LambdaHack.Ability: instance Ord Ability
- Game.LambdaHack.Ability: instance Show Ability
- Game.LambdaHack.Action: abort :: Action a
- Game.LambdaHack.Action: abortIfWith :: Bool -> Msg -> Action a
- Game.LambdaHack.Action: abortWith :: Msg -> Action a
- Game.LambdaHack.Action: data Action a
- Game.LambdaHack.Action: debug :: Text -> Action ()
- Game.LambdaHack.Action: displayChoiceUI :: Msg -> [Overlay] -> [(Key, Modifier)] -> Action (Key, Modifier)
- Game.LambdaHack.Action: displayFramePush :: Maybe SingleFrame -> Action ()
- Game.LambdaHack.Action: displayMore :: ColorMode -> Msg -> Action Bool
- Game.LambdaHack.Action: displayOverAbort :: Msg -> [Overlay] -> Action ()
- Game.LambdaHack.Action: displayOverlays :: Msg -> Msg -> [Overlay] -> ActionFrame ()
- Game.LambdaHack.Action: displayYesNo :: Msg -> Action Bool
- Game.LambdaHack.Action: drawPrompt :: ColorMode -> Msg -> Action SingleFrame
- Game.LambdaHack.Action: dumpCfg :: FilePath -> Action ()
- Game.LambdaHack.Action: endOrLoop :: Action () -> Action ()
- Game.LambdaHack.Action: frontendName :: String
- Game.LambdaHack.Action: getBinding :: Action (Binding (ActionFrame ()))
- Game.LambdaHack.Action: getCOps :: Action COps
- Game.LambdaHack.Action: getConfigUI :: Action ConfigUI
- Game.LambdaHack.Action: getDiary :: Action Diary
- Game.LambdaHack.Action: getKeyCommand :: Maybe Bool -> Action (Key, Modifier)
- Game.LambdaHack.Action: getKeyFrameCommand :: SingleFrame -> Action (Key, Modifier)
- Game.LambdaHack.Action: getOverConfirm :: [SingleFrame] -> Action Bool
- Game.LambdaHack.Action: getPerception :: Action Perception
- Game.LambdaHack.Action: inFrame :: Action () -> ActionFrame ()
- Game.LambdaHack.Action: msgAdd :: Msg -> Action ()
- Game.LambdaHack.Action: neverMind :: Bool -> Action a
- Game.LambdaHack.Action: recordHistory :: Action ()
- Game.LambdaHack.Action: remember :: Action ()
- Game.LambdaHack.Action: rememberList :: [Point] -> Action ()
- Game.LambdaHack.Action: returnFrame :: SingleFrame -> ActionFrame ()
- Game.LambdaHack.Action: returnNoFrame :: a -> ActionFrame a
- Game.LambdaHack.Action: saveGameBkp :: Action ()
- Game.LambdaHack.Action: startClip :: Action () -> Action ()
- Game.LambdaHack.Action: startFrontend :: COps -> (ConfigUI -> Binding (ActionFrame ())) -> Action () -> IO ()
- Game.LambdaHack.Action: tryIgnore :: Action () -> Action ()
- Game.LambdaHack.Action: tryRepeatedlyWith :: (Msg -> Action ()) -> Action () -> Action ()
- Game.LambdaHack.Action: tryWith :: (Msg -> Action a) -> Action a -> Action a
- Game.LambdaHack.Action: tryWithFrame :: Action a -> ActionFrame a -> ActionFrame a
- Game.LambdaHack.Action: type ActionFrame a = Action (a, [Maybe SingleFrame])
- Game.LambdaHack.Action: whenFrame :: Bool -> ActionFrame () -> ActionFrame ()
- Game.LambdaHack.Action.ActionLift: Session :: FrontendSession -> COps -> Binding (ActionFrame ()) -> ConfigUI -> Session
- Game.LambdaHack.Action.ActionLift: abort :: Action a
- Game.LambdaHack.Action.ActionLift: abortIfWith :: Bool -> Msg -> Action a
- Game.LambdaHack.Action.ActionLift: abortWith :: Msg -> Action a
- Game.LambdaHack.Action.ActionLift: data Action a
- Game.LambdaHack.Action.ActionLift: data Session
- Game.LambdaHack.Action.ActionLift: getBinding :: Action (Binding (ActionFrame ()))
- Game.LambdaHack.Action.ActionLift: getCOps :: Action COps
- Game.LambdaHack.Action.ActionLift: getConfigUI :: Action ConfigUI
- Game.LambdaHack.Action.ActionLift: getDiary :: Action Diary
- Game.LambdaHack.Action.ActionLift: getFrontendSession :: Action FrontendSession
- Game.LambdaHack.Action.ActionLift: getPerception :: Action Perception
- Game.LambdaHack.Action.ActionLift: handlerToIO :: Session -> State -> Diary -> Action () -> IO ()
- Game.LambdaHack.Action.ActionLift: historyReset :: History -> Action ()
- Game.LambdaHack.Action.ActionLift: inFrame :: Action () -> ActionFrame ()
- Game.LambdaHack.Action.ActionLift: instance Functor Action
- Game.LambdaHack.Action.ActionLift: instance Monad Action
- Game.LambdaHack.Action.ActionLift: instance MonadState State Action
- Game.LambdaHack.Action.ActionLift: instance Show (Action a)
- Game.LambdaHack.Action.ActionLift: liftIO :: IO a -> Action a
- Game.LambdaHack.Action.ActionLift: msgAdd :: Msg -> Action ()
- Game.LambdaHack.Action.ActionLift: msgReset :: Msg -> Action ()
- Game.LambdaHack.Action.ActionLift: neverMind :: Bool -> Action a
- Game.LambdaHack.Action.ActionLift: returnFrame :: SingleFrame -> ActionFrame ()
- Game.LambdaHack.Action.ActionLift: returnNoFrame :: a -> ActionFrame a
- Game.LambdaHack.Action.ActionLift: sbinding :: Session -> Binding (ActionFrame ())
- Game.LambdaHack.Action.ActionLift: sconfigUI :: Session -> ConfigUI
- Game.LambdaHack.Action.ActionLift: scops :: Session -> COps
- Game.LambdaHack.Action.ActionLift: sfs :: Session -> FrontendSession
- Game.LambdaHack.Action.ActionLift: tryIgnore :: Action () -> Action ()
- Game.LambdaHack.Action.ActionLift: tryRepeatedlyWith :: (Msg -> Action ()) -> Action () -> Action ()
- Game.LambdaHack.Action.ActionLift: tryWith :: (Msg -> Action a) -> Action a -> Action a
- Game.LambdaHack.Action.ActionLift: type ActionFrame a = Action (a, [Maybe SingleFrame])
- Game.LambdaHack.Action.ActionLift: type ActionFun r a = Session -> DungeonPerception -> (State -> Diary -> a -> IO r) -> (Msg -> IO r) -> State -> Diary -> IO r
- Game.LambdaHack.Action.ActionLift: whenFrame :: Bool -> ActionFrame () -> ActionFrame ()
- Game.LambdaHack.Action.ActionLift: withPerception :: Action () -> Action ()
- Game.LambdaHack.Action.ConfigIO: dump :: Config -> FilePath -> IO ()
- Game.LambdaHack.Action.ConfigIO: instance Show CP
- Game.LambdaHack.Action.ConfigIO: mkConfigRules :: Ops RuleKind -> IO (Config, StdGen, StdGen)
- Game.LambdaHack.Action.ConfigIO: mkConfigUI :: Ops RuleKind -> IO ConfigUI
- Game.LambdaHack.Action.Frontend: displayFrame :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()
- Game.LambdaHack.Action.Frontend: frontendName :: String
- Game.LambdaHack.Action.Frontend: nextEvent :: FrontendSession -> Maybe Bool -> IO (Key, Modifier)
- Game.LambdaHack.Action.Frontend: promptGetKey :: FrontendSession -> [(Key, Modifier)] -> SingleFrame -> IO (Key, Modifier)
- Game.LambdaHack.Action.Frontend: startup :: String -> (FrontendSession -> IO ()) -> IO ()
- Game.LambdaHack.Action.Frontend: type FrontendSession = ()
- Game.LambdaHack.Action.Frontend.Chosen: display :: FrontendSession -> Bool -> Bool -> Maybe SingleFrame -> IO ()
- Game.LambdaHack.Action.Frontend.Chosen: frontendName :: String
- Game.LambdaHack.Action.Frontend.Chosen: nextEvent :: FrontendSession -> Maybe Bool -> IO (Key, Modifier)
- Game.LambdaHack.Action.Frontend.Chosen: promptGetAnyKey :: FrontendSession -> SingleFrame -> IO (Key, Modifier)
- Game.LambdaHack.Action.Frontend.Chosen: startup :: String -> (FrontendSession -> IO ()) -> IO ()
- Game.LambdaHack.Action.Frontend.Chosen: type FrontendSession = ()
- Game.LambdaHack.Action.HighScore: instance Binary ScoreRecord
- Game.LambdaHack.Action.HighScore: instance Eq ScoreRecord
- Game.LambdaHack.Action.HighScore: instance Ord ScoreRecord
- Game.LambdaHack.Action.HighScore: register :: ConfigUI -> Bool -> Int -> Time -> ClockTime -> Status -> IO (Msg, [Overlay])
- Game.LambdaHack.Action.Save: restoreGame :: ConfigUI -> (FilePath -> IO FilePath) -> Text -> IO (Either (State, Diary, Msg) (Diary, Msg))
- Game.LambdaHack.Action.Save: rmBkpSaveDiary :: ConfigUI -> Diary -> IO ()
- Game.LambdaHack.Action.Save: saveGameBkp :: ConfigUI -> State -> Diary -> IO ()
- Game.LambdaHack.Action.Save: saveGameFile :: ConfigUI -> State -> IO ()
- Game.LambdaHack.Actions: actorAttackActor :: ActorId -> ActorId -> Action ()
- Game.LambdaHack.Actions: actorOpenDoor :: ActorId -> Vector -> Action ()
- Game.LambdaHack.Actions: actorRunActor :: ActorId -> ActorId -> Action ()
- Game.LambdaHack.Actions: addSmell :: Action ()
- Game.LambdaHack.Actions: backCycleHero :: Action ()
- Game.LambdaHack.Actions: bumpTile :: Point -> Feature -> Action ()
- Game.LambdaHack.Actions: cycleHero :: Action ()
- Game.LambdaHack.Actions: displayHelp :: ActionFrame ()
- Game.LambdaHack.Actions: displayHistory :: ActionFrame ()
- Game.LambdaHack.Actions: displayMainMenu :: ActionFrame ()
- Game.LambdaHack.Actions: dumpConfig :: Action ()
- Game.LambdaHack.Actions: gameExit :: Action ()
- Game.LambdaHack.Actions: gameRestart :: Action ()
- Game.LambdaHack.Actions: gameSave :: Action ()
- Game.LambdaHack.Actions: generateMonster :: Action ()
- Game.LambdaHack.Actions: guessBump :: Ops TileKind -> Feature -> Id TileKind -> Action ()
- Game.LambdaHack.Actions: heroesAfterPl :: Action [ActorId]
- Game.LambdaHack.Actions: ifRunning :: ((Vector, Int) -> Action a) -> Action a -> Action a
- Game.LambdaHack.Actions: move :: Vector -> ActionFrame ()
- Game.LambdaHack.Actions: moveCursor :: Vector -> Int -> ActionFrame ()
- Game.LambdaHack.Actions: moveOrAttack :: Bool -> ActorId -> Vector -> Action ()
- Game.LambdaHack.Actions: playerBumpDir :: Feature -> Vector -> Action ()
- Game.LambdaHack.Actions: playerTriggerDir :: Feature -> Part -> Action ()
- Game.LambdaHack.Actions: playerTriggerTile :: Feature -> Action ()
- Game.LambdaHack.Actions: regenerateLevelHP :: Action ()
- Game.LambdaHack.Actions: rollMonster :: COps -> Perception -> State -> Rnd State
- Game.LambdaHack.Actions: search :: Action ()
- Game.LambdaHack.Actions: setWaitBlock :: ActorId -> Action ()
- Game.LambdaHack.Actions: tgtAscend :: Int -> ActionFrame ()
- Game.LambdaHack.Actions: triggerTile :: Point -> Action ()
- Game.LambdaHack.Actions: waitBlock :: Action ()
- Game.LambdaHack.Actor: Actor :: !(Id ActorKind) -> !(Maybe Char) -> !(Maybe Text) -> !(Maybe Color) -> !(Maybe Speed) -> !Int -> !(Maybe (Vector, Int)) -> Target -> !Point -> !Char -> !Time -> !Time -> !(Id FactionKind) -> !Bool -> Actor
- Game.LambdaHack.Actor: TCursor :: Target
- Game.LambdaHack.Actor: TEnemy :: ActorId -> Point -> Target
- Game.LambdaHack.Actor: TLoc :: Point -> Target
- Game.LambdaHack.Actor: TPath :: [Vector] -> Target
- Game.LambdaHack.Actor: actorSpeed :: Ops ActorKind -> Actor -> Speed
- Game.LambdaHack.Actor: addHp :: Ops ActorKind -> Int -> Actor -> Actor
- Game.LambdaHack.Actor: bcolor :: Actor -> !(Maybe Color)
- Game.LambdaHack.Actor: bdir :: Actor -> !(Maybe (Vector, Int))
- Game.LambdaHack.Actor: bfaction :: Actor -> !(Id FactionKind)
- Game.LambdaHack.Actor: bhp :: Actor -> !Int
- Game.LambdaHack.Actor: bkind :: Actor -> !(Id ActorKind)
- Game.LambdaHack.Actor: bletter :: Actor -> !Char
- Game.LambdaHack.Actor: bloc :: Actor -> !Point
- Game.LambdaHack.Actor: bname :: Actor -> !(Maybe Text)
- Game.LambdaHack.Actor: bproj :: Actor -> !Bool
- Game.LambdaHack.Actor: braced :: Actor -> Time -> Bool
- Game.LambdaHack.Actor: bspeed :: Actor -> !(Maybe Speed)
- Game.LambdaHack.Actor: bsymbol :: Actor -> !(Maybe Char)
- Game.LambdaHack.Actor: btarget :: Actor -> Target
- Game.LambdaHack.Actor: btime :: Actor -> !Time
- Game.LambdaHack.Actor: bwait :: Actor -> !Time
- Game.LambdaHack.Actor: data Actor
- Game.LambdaHack.Actor: data Target
- Game.LambdaHack.Actor: findHeroName :: ConfigUI -> Int -> Text
- Game.LambdaHack.Actor: heroKindId :: Ops ActorKind -> Id ActorKind
- Game.LambdaHack.Actor: instance Binary Actor
- Game.LambdaHack.Actor: instance Binary Target
- Game.LambdaHack.Actor: instance Eq Target
- Game.LambdaHack.Actor: instance Show Actor
- Game.LambdaHack.Actor: instance Show Target
- Game.LambdaHack.Actor: monsterGenChance :: Int -> Int -> Rnd Bool
- Game.LambdaHack.Actor: partActor :: Ops ActorKind -> Actor -> Part
- Game.LambdaHack.Actor: projectileKindId :: Ops ActorKind -> Id ActorKind
- Game.LambdaHack.Actor: template :: Id ActorKind -> Maybe Char -> Maybe Text -> Int -> Point -> Time -> Id FactionKind -> Bool -> Actor
- Game.LambdaHack.Actor: timeAddFromSpeed :: Ops ActorKind -> Actor -> Time -> Time
- Game.LambdaHack.Actor: type ActorId = Int
- Game.LambdaHack.Actor: unoccupied :: [Actor] -> Point -> Bool
- Game.LambdaHack.ActorState: addHero :: COps -> Point -> ConfigUI -> State -> State
- Game.LambdaHack.ActorState: addMonster :: Ops TileKind -> Id ActorKind -> Int -> Point -> Id FactionKind -> Bool -> State -> State
- Game.LambdaHack.ActorState: addProjectile :: COps -> Item -> Point -> Id FactionKind -> [Point] -> Time -> State -> State
- Game.LambdaHack.ActorState: allHeroesAnyLevel :: State -> [ActorId]
- Game.LambdaHack.ActorState: calculateTotal :: Ops ItemKind -> State -> ([Item], Int)
- Game.LambdaHack.ActorState: dangerousList :: State -> [Actor]
- Game.LambdaHack.ActorState: deleteActor :: ActorId -> State -> State
- Game.LambdaHack.ActorState: deletePlayer :: State -> State
- Game.LambdaHack.ActorState: factionList :: [Id FactionKind] -> State -> [Actor]
- Game.LambdaHack.ActorState: findActorAnyLevel :: ActorId -> State -> (LevelId, Actor, [Item])
- Game.LambdaHack.ActorState: foesAdjacent :: X -> Y -> Point -> [Actor] -> Bool
- Game.LambdaHack.ActorState: getActor :: ActorId -> State -> Actor
- Game.LambdaHack.ActorState: getActorItem :: ActorId -> State -> [Item]
- Game.LambdaHack.ActorState: getPlayerBody :: State -> Actor
- Game.LambdaHack.ActorState: getPlayerItem :: State -> [Item]
- Game.LambdaHack.ActorState: heroList :: State -> [Actor]
- Game.LambdaHack.ActorState: hostileAssocs :: Id FactionKind -> Level -> [(ActorId, Actor)]
- Game.LambdaHack.ActorState: hostileList :: State -> [Actor]
- Game.LambdaHack.ActorState: initialHeroes :: COps -> Point -> ConfigUI -> State -> State
- Game.LambdaHack.ActorState: insertActor :: ActorId -> Actor -> State -> State
- Game.LambdaHack.ActorState: isAHero :: State -> ActorId -> Bool
- Game.LambdaHack.ActorState: isProjectile :: State -> ActorId -> Bool
- Game.LambdaHack.ActorState: locToActor :: Point -> State -> Maybe ActorId
- Game.LambdaHack.ActorState: memActor :: ActorId -> State -> Bool
- Game.LambdaHack.ActorState: smellTimeout :: State -> Time
- Game.LambdaHack.ActorState: targetToLoc :: IntSet -> State -> Point -> Maybe Point
- Game.LambdaHack.ActorState: tryFindHeroK :: State -> Int -> Maybe ActorId
- Game.LambdaHack.ActorState: updateAnyActorBody :: ActorId -> (Actor -> Actor) -> State -> State
- Game.LambdaHack.ActorState: updateAnyActorItem :: ActorId -> ([Item] -> [Item]) -> State -> State
- Game.LambdaHack.Animation: Attr :: !Color -> !Color -> Attr
- Game.LambdaHack.Animation: AttrChar :: !Attr -> !Char -> AttrChar
- Game.LambdaHack.Animation: SingleFrame :: ![[AttrChar]] -> Text -> Text -> SingleFrame
- Game.LambdaHack.Animation: acAttr :: AttrChar -> !Attr
- Game.LambdaHack.Animation: acChar :: AttrChar -> !Char
- Game.LambdaHack.Animation: bg :: Attr -> !Color
- Game.LambdaHack.Animation: blockHit :: (Maybe Point, Maybe Point) -> Color -> Color -> Animation
- Game.LambdaHack.Animation: blockMiss :: (Maybe Point, Maybe Point) -> Animation
- Game.LambdaHack.Animation: data Animation
- Game.LambdaHack.Animation: data Attr
- Game.LambdaHack.Animation: data AttrChar
- Game.LambdaHack.Animation: data SingleFrame
- Game.LambdaHack.Animation: deathBody :: Point -> Animation
- Game.LambdaHack.Animation: defaultAttr :: Attr
- Game.LambdaHack.Animation: fg :: Attr -> !Color
- Game.LambdaHack.Animation: instance Eq SingleFrame
- Game.LambdaHack.Animation: instance Monoid Animation
- Game.LambdaHack.Animation: rederAnim :: X -> Y -> SingleFrame -> Animation -> [Maybe SingleFrame]
- Game.LambdaHack.Animation: sfBottom :: SingleFrame -> Text
- Game.LambdaHack.Animation: sfLevel :: SingleFrame -> ![[AttrChar]]
- Game.LambdaHack.Animation: sfTop :: SingleFrame -> Text
- Game.LambdaHack.Animation: swapPlaces :: (Maybe Point, Maybe Point) -> Animation
- Game.LambdaHack.Animation: twirlSplash :: (Maybe Point, Maybe Point) -> Color -> Color -> Animation
- Game.LambdaHack.Area: expand :: Area -> Int -> Area
- Game.LambdaHack.Area: grid :: (X, Y) -> Area -> [(PointXY, Area)]
- Game.LambdaHack.Area: insideXY :: PointXY -> Area -> Bool
- Game.LambdaHack.Area: normalizeArea :: Area -> Area
- Game.LambdaHack.Area: trivialArea :: Area -> Bool
- Game.LambdaHack.Area: type Area = (X, Y, X, Y)
- Game.LambdaHack.Area: validArea :: Area -> Bool
- Game.LambdaHack.Area: vicinityCardinalXY :: Area -> PointXY -> [PointXY]
- Game.LambdaHack.Area: vicinityXY :: Area -> PointXY -> [PointXY]
- Game.LambdaHack.AreaRnd: connectGrid :: (X, Y) -> Rnd [(PointXY, PointXY)]
- Game.LambdaHack.AreaRnd: connectPlaces :: Area -> Area -> Rnd Corridor
- Game.LambdaHack.AreaRnd: mkRoom :: (X, Y) -> Area -> Rnd Area
- Game.LambdaHack.AreaRnd: mkVoidRoom :: Area -> Rnd Area
- Game.LambdaHack.AreaRnd: randomConnection :: (X, Y) -> Rnd (PointXY, PointXY)
- Game.LambdaHack.AreaRnd: type Corridor = [PointXY]
- Game.LambdaHack.AreaRnd: xyInArea :: Area -> Rnd PointXY
- Game.LambdaHack.Binding: Binding :: Map (Key, Modifier) (Text, Bool, a) -> Map Key Key -> [Key] -> [(Key, Modifier)] -> Map Cmd Key -> Binding a
- Game.LambdaHack.Binding: data Binding a
- Game.LambdaHack.Binding: kcmd :: Binding a -> Map (Key, Modifier) (Text, Bool, a)
- Game.LambdaHack.Binding: kdir :: Binding a -> [(Key, Modifier)]
- Game.LambdaHack.Binding: keyHelp :: Binding a -> [Overlay]
- Game.LambdaHack.Binding: kmacro :: Binding a -> Map Key Key
- Game.LambdaHack.Binding: kmajor :: Binding a -> [Key]
- Game.LambdaHack.Binding: krevMap :: Binding a -> Map Cmd Key
- Game.LambdaHack.BindingAction: stdBinding :: ConfigUI -> Binding (ActionFrame ())
- Game.LambdaHack.CDefs: CDefs :: (a -> Char) -> (a -> Text) -> (a -> Freqs) -> ([a] -> [a]) -> [a] -> CDefs a
- Game.LambdaHack.CDefs: content :: CDefs a -> [a]
- Game.LambdaHack.CDefs: data CDefs a
- Game.LambdaHack.CDefs: getFreq :: CDefs a -> a -> Freqs
- Game.LambdaHack.CDefs: getName :: CDefs a -> a -> Text
- Game.LambdaHack.CDefs: getSymbol :: CDefs a -> a -> Char
- Game.LambdaHack.CDefs: validate :: CDefs a -> [a] -> [a]
- Game.LambdaHack.Cave: Cave :: !(Id CaveKind) -> TileMapXY -> SecretMapXY -> ItemMapXY -> Text -> [Place] -> Cave
- Game.LambdaHack.Cave: buildCave :: COps -> Int -> Int -> Id CaveKind -> Rnd Cave
- Game.LambdaHack.Cave: data Cave
- Game.LambdaHack.Cave: ditem :: Cave -> ItemMapXY
- Game.LambdaHack.Cave: dkind :: Cave -> !(Id CaveKind)
- Game.LambdaHack.Cave: dmap :: Cave -> TileMapXY
- Game.LambdaHack.Cave: dmeta :: Cave -> Text
- Game.LambdaHack.Cave: dplaces :: Cave -> [Place]
- Game.LambdaHack.Cave: dsecret :: Cave -> SecretMapXY
- Game.LambdaHack.Cave: instance Show Cave
- Game.LambdaHack.Cave: type ItemMapXY = Map PointXY Item
- Game.LambdaHack.Cave: type SecretMapXY = Map PointXY SecretTime
- Game.LambdaHack.Cave: type TileMapXY = TileMapXY
- Game.LambdaHack.Color: Attr :: !Color -> !Color -> Attr
- Game.LambdaHack.Color: AttrChar :: !Attr -> !Char -> AttrChar
- Game.LambdaHack.Color: Black :: Color
- Game.LambdaHack.Color: Blue :: Color
- Game.LambdaHack.Color: BrBlack :: Color
- Game.LambdaHack.Color: BrBlue :: Color
- Game.LambdaHack.Color: BrCyan :: Color
- Game.LambdaHack.Color: BrGreen :: Color
- Game.LambdaHack.Color: BrMagenta :: Color
- Game.LambdaHack.Color: BrRed :: Color
- Game.LambdaHack.Color: BrWhite :: Color
- Game.LambdaHack.Color: BrYellow :: Color
- Game.LambdaHack.Color: Brown :: Color
- Game.LambdaHack.Color: Cyan :: Color
- Game.LambdaHack.Color: Green :: Color
- Game.LambdaHack.Color: Magenta :: Color
- Game.LambdaHack.Color: Red :: Color
- Game.LambdaHack.Color: White :: Color
- Game.LambdaHack.Color: acAttr :: AttrChar -> !Attr
- Game.LambdaHack.Color: acChar :: AttrChar -> !Char
- Game.LambdaHack.Color: bg :: Attr -> !Color
- Game.LambdaHack.Color: colorToRGB :: Color -> String
- Game.LambdaHack.Color: data Attr
- Game.LambdaHack.Color: data AttrChar
- Game.LambdaHack.Color: data Color
- Game.LambdaHack.Color: defBG :: Color
- Game.LambdaHack.Color: defFG :: Color
- Game.LambdaHack.Color: defaultAttr :: Attr
- Game.LambdaHack.Color: fg :: Attr -> !Color
- Game.LambdaHack.Color: instance Binary Attr
- Game.LambdaHack.Color: instance Binary AttrChar
- Game.LambdaHack.Color: instance Binary Color
- Game.LambdaHack.Color: instance Bounded Color
- Game.LambdaHack.Color: instance Enum Color
- Game.LambdaHack.Color: instance Eq Attr
- Game.LambdaHack.Color: instance Eq AttrChar
- Game.LambdaHack.Color: instance Eq Color
- Game.LambdaHack.Color: instance Ord Attr
- Game.LambdaHack.Color: instance Ord Color
- Game.LambdaHack.Color: instance Show Attr
- Game.LambdaHack.Color: instance Show AttrChar
- Game.LambdaHack.Color: instance Show Color
- Game.LambdaHack.Color: isBright :: Color -> Bool
- Game.LambdaHack.Color: legalBG :: [Color]
- Game.LambdaHack.Command: Accept :: Cmd
- Game.LambdaHack.Command: Apply :: Part -> Part -> [Char] -> Cmd
- Game.LambdaHack.Command: Cancel :: Cmd
- Game.LambdaHack.Command: CfgDump :: Cmd
- Game.LambdaHack.Command: Clear :: Cmd
- Game.LambdaHack.Command: Drop :: Cmd
- Game.LambdaHack.Command: EpsIncr :: Bool -> Cmd
- Game.LambdaHack.Command: GameExit :: Cmd
- Game.LambdaHack.Command: GameRestart :: Cmd
- Game.LambdaHack.Command: GameSave :: Cmd
- Game.LambdaHack.Command: Help :: Cmd
- Game.LambdaHack.Command: HeroBack :: Cmd
- Game.LambdaHack.Command: HeroCycle :: Cmd
- Game.LambdaHack.Command: History :: Cmd
- Game.LambdaHack.Command: Inventory :: Cmd
- Game.LambdaHack.Command: Pickup :: Cmd
- Game.LambdaHack.Command: Project :: Part -> Part -> [Char] -> Cmd
- Game.LambdaHack.Command: TgtAscend :: Int -> Cmd
- Game.LambdaHack.Command: TgtEnemy :: Cmd
- Game.LambdaHack.Command: TgtFloor :: Cmd
- Game.LambdaHack.Command: TriggerDir :: Part -> Part -> Feature -> Cmd
- Game.LambdaHack.Command: TriggerTile :: Part -> Part -> Feature -> Cmd
- Game.LambdaHack.Command: Wait :: Cmd
- Game.LambdaHack.Command: cmdDescription :: Cmd -> Text
- Game.LambdaHack.Command: data Cmd
- Game.LambdaHack.Command: feature :: Cmd -> Feature
- Game.LambdaHack.Command: instance Eq Cmd
- Game.LambdaHack.Command: instance Ord Cmd
- Game.LambdaHack.Command: instance Read Cmd
- Game.LambdaHack.Command: instance Show Cmd
- Game.LambdaHack.Command: majorCmd :: Cmd -> Bool
- Game.LambdaHack.Command: object :: Cmd -> Part
- Game.LambdaHack.Command: syms :: Cmd -> [Char]
- Game.LambdaHack.Command: timedCmd :: Cmd -> Bool
- Game.LambdaHack.Command: verb :: Cmd -> Part
- Game.LambdaHack.CommandAction: configCmds :: ConfigUI -> [(Key, Cmd)]
- Game.LambdaHack.CommandAction: semanticsCmds :: [(Key, Cmd)] -> [((Key, Modifier), (Text, Bool, ActionFrame ()))]
- Game.LambdaHack.Config: Blind :: FovMode
- Game.LambdaHack.Config: Config :: !String -> ![(Text, Text)] -> !Int -> !FovMode -> !Int -> !Int -> !Int -> !Bool -> !Text -> Config
- Game.LambdaHack.Config: ConfigUI :: ![(Key, String)] -> !FilePath -> !FilePath -> !FilePath -> !FilePath -> !FilePath -> !FilePath -> !FilePath -> ![(Int, Text)] -> ![(Key, Key)] -> !String -> !Int -> ConfigUI
- Game.LambdaHack.Config: Digital :: Int -> FovMode
- Game.LambdaHack.Config: Permissive :: FovMode
- Game.LambdaHack.Config: Shadow :: FovMode
- Game.LambdaHack.Config: configAppDataDir :: ConfigUI -> !FilePath
- Game.LambdaHack.Config: configBaseHP :: Config -> !Int
- Game.LambdaHack.Config: configBkpFile :: ConfigUI -> !FilePath
- Game.LambdaHack.Config: configCaves :: Config -> ![(Text, Text)]
- Game.LambdaHack.Config: configCommands :: ConfigUI -> ![(Key, String)]
- Game.LambdaHack.Config: configDepth :: Config -> !Int
- Game.LambdaHack.Config: configDiaryFile :: ConfigUI -> !FilePath
- Game.LambdaHack.Config: configExtraHeroes :: Config -> !Int
- Game.LambdaHack.Config: configFaction :: Config -> !Text
- Game.LambdaHack.Config: configFirstDeathEnds :: Config -> !Bool
- Game.LambdaHack.Config: configFont :: ConfigUI -> !String
- Game.LambdaHack.Config: configFovMode :: Config -> !FovMode
- Game.LambdaHack.Config: configHeroNames :: ConfigUI -> ![(Int, Text)]
- Game.LambdaHack.Config: configHistoryMax :: ConfigUI -> !Int
- Game.LambdaHack.Config: configMacros :: ConfigUI -> ![(Key, Key)]
- Game.LambdaHack.Config: configRulesCfgFile :: ConfigUI -> !FilePath
- Game.LambdaHack.Config: configSaveFile :: ConfigUI -> !FilePath
- Game.LambdaHack.Config: configScoresFile :: ConfigUI -> !FilePath
- Game.LambdaHack.Config: configSelfString :: Config -> !String
- Game.LambdaHack.Config: configSmellTimeout :: Config -> !Int
- Game.LambdaHack.Config: configUICfgFile :: ConfigUI -> !FilePath
- Game.LambdaHack.Config: data Config
- Game.LambdaHack.Config: data ConfigUI
- Game.LambdaHack.Config: data FovMode
- Game.LambdaHack.Config: instance Binary Config
- Game.LambdaHack.Config: instance Binary ConfigUI
- Game.LambdaHack.Config: instance Binary FovMode
- Game.LambdaHack.Config: instance Read FovMode
- Game.LambdaHack.Config: instance Show Config
- Game.LambdaHack.Config: instance Show ConfigUI
- Game.LambdaHack.Config: instance Show FovMode
- Game.LambdaHack.Content.CaveKind: cdefaultTile :: CaveKind -> Text
- Game.LambdaHack.Content.CaveKind: chiddenChance :: CaveKind -> Chance
- Game.LambdaHack.Content.CaveKind: chiddenTile :: CaveKind -> Text
- Game.LambdaHack.Content.FactionKind: fAiIdle :: FactionKind -> !Text
- Game.LambdaHack.Content.FactionKind: fAiSelected :: FactionKind -> !Text
- Game.LambdaHack.Content.FactionKind: fally :: FactionKind -> ![Text]
- Game.LambdaHack.Content.FactionKind: fenemy :: FactionKind -> ![Text]
- Game.LambdaHack.Content.ItemKind: ipower :: ItemKind -> !RollDeep
- Game.LambdaHack.Draw: ColorBW :: ColorMode
- Game.LambdaHack.Draw: ColorFull :: ColorMode
- Game.LambdaHack.Draw: animate :: State -> Diary -> COps -> Perception -> Animation -> [Maybe SingleFrame]
- Game.LambdaHack.Draw: data ColorMode
- Game.LambdaHack.Draw: draw :: ColorMode -> COps -> Perception -> State -> Overlay -> SingleFrame
- Game.LambdaHack.Dungeon: (!) :: Dungeon -> LevelId -> Level
- Game.LambdaHack.Dungeon: adjust :: (Level -> Level) -> LevelId -> Dungeon -> Dungeon
- Game.LambdaHack.Dungeon: currentFirst :: LevelId -> Dungeon -> [(LevelId, Level)]
- Game.LambdaHack.Dungeon: data Dungeon
- Game.LambdaHack.Dungeon: data LevelId
- Game.LambdaHack.Dungeon: depth :: Dungeon -> Int
- Game.LambdaHack.Dungeon: fromList :: [(LevelId, Level)] -> Int -> Dungeon
- Game.LambdaHack.Dungeon: instance Binary Dungeon
- Game.LambdaHack.Dungeon: instance Binary LevelId
- Game.LambdaHack.Dungeon: instance Eq LevelId
- Game.LambdaHack.Dungeon: instance Ord LevelId
- Game.LambdaHack.Dungeon: instance Show Dungeon
- Game.LambdaHack.Dungeon: instance Show LevelId
- Game.LambdaHack.Dungeon: levelDefault :: Int -> LevelId
- Game.LambdaHack.Dungeon: levelNumber :: LevelId -> Int
- Game.LambdaHack.Dungeon: lookup :: LevelId -> Dungeon -> Maybe Level
- Game.LambdaHack.Dungeon: mapDungeon :: (Level -> Level) -> Dungeon -> Dungeon
- Game.LambdaHack.DungeonState: FreshDungeon :: LevelId -> Point -> Dungeon -> FreshDungeon
- Game.LambdaHack.DungeonState: data FreshDungeon
- Game.LambdaHack.DungeonState: entryLevel :: FreshDungeon -> LevelId
- Game.LambdaHack.DungeonState: entryLoc :: FreshDungeon -> Point
- Game.LambdaHack.DungeonState: freshDungeon :: FreshDungeon -> Dungeon
- Game.LambdaHack.DungeonState: generate :: COps -> Config -> Rnd FreshDungeon
- Game.LambdaHack.DungeonState: whereTo :: State -> Int -> Maybe (LevelId, Point)
- Game.LambdaHack.Effect: ApplyPerfume :: Effect
- Game.LambdaHack.Effect: Ascend :: Effect
- Game.LambdaHack.Effect: Descend :: Effect
- Game.LambdaHack.Effect: Dominate :: Effect
- Game.LambdaHack.Effect: Heal :: Effect
- Game.LambdaHack.Effect: NoEffect :: Effect
- Game.LambdaHack.Effect: Regeneration :: Effect
- Game.LambdaHack.Effect: Searching :: Effect
- Game.LambdaHack.Effect: SummonEnemy :: Effect
- Game.LambdaHack.Effect: SummonFriend :: Effect
- Game.LambdaHack.Effect: Wound :: !RollDice -> Effect
- Game.LambdaHack.Effect: data Effect
- Game.LambdaHack.Effect: effectToBenefit :: Effect -> Int
- Game.LambdaHack.Effect: effectToSuffix :: Effect -> Text
- Game.LambdaHack.Effect: instance Eq Effect
- Game.LambdaHack.Effect: instance Ord Effect
- Game.LambdaHack.Effect: instance Read Effect
- Game.LambdaHack.Effect: instance Show Effect
- Game.LambdaHack.EffectAction: actorVerb :: Ops ActorKind -> Actor -> Text -> Text
- Game.LambdaHack.EffectAction: checkPartyDeath :: Action ()
- Game.LambdaHack.EffectAction: discover :: Item -> Action ()
- Game.LambdaHack.EffectAction: doLook :: ActionFrame ()
- Game.LambdaHack.EffectAction: eff :: Effect -> Int -> ActorId -> ActorId -> Int -> Action (Bool, Text)
- Game.LambdaHack.EffectAction: effLvlGoUp :: Int -> Action ()
- Game.LambdaHack.EffectAction: effectToAction :: Effect -> Int -> ActorId -> ActorId -> Int -> Bool -> Action (Bool, Bool)
- Game.LambdaHack.EffectAction: fleeDungeon :: Action ()
- Game.LambdaHack.EffectAction: focusIfOurs :: ActorId -> Action Bool
- Game.LambdaHack.EffectAction: gameOver :: Bool -> Action ()
- Game.LambdaHack.EffectAction: itemEffectAction :: Int -> ActorId -> ActorId -> Item -> Bool -> Action ()
- Game.LambdaHack.EffectAction: itemOverlay :: Bool -> Bool -> [Item] -> Action [Overlay]
- Game.LambdaHack.EffectAction: nullEffect :: Action (Bool, Text)
- Game.LambdaHack.EffectAction: rndToAction :: Rnd a -> Action a
- Game.LambdaHack.EffectAction: selectPlayer :: ActorId -> Action Bool
- Game.LambdaHack.EffectAction: squashActor :: ActorId -> ActorId -> Action ()
- Game.LambdaHack.EffectAction: stopRunning :: Action ()
- Game.LambdaHack.EffectAction: summonHeroes :: Int -> Point -> Action ()
- Game.LambdaHack.EffectAction: summonMonsters :: Int -> Point -> Action ()
- Game.LambdaHack.EffectAction: switchLevel :: LevelId -> Action ()
- Game.LambdaHack.EffectAction: updateAnyActor :: ActorId -> (Actor -> Actor) -> Action ()
- Game.LambdaHack.EffectAction: updatePlayerBody :: (Actor -> Actor) -> Action ()
- Game.LambdaHack.FOV: fullscan :: Ops TileKind -> FovMode -> Point -> Level -> [Point]
- Game.LambdaHack.FOV.Common: B :: (X, Y) -> Bump
- Game.LambdaHack.FOV.Common: addHull :: (Bump -> Bump -> Bool) -> Bump -> ConvexHull -> ConvexHull
- Game.LambdaHack.FOV.Common: instance Show Bump
- Game.LambdaHack.FOV.Common: maximal :: (a -> a -> Bool) -> [a] -> a
- Game.LambdaHack.FOV.Common: newtype Bump
- Game.LambdaHack.FOV.Common: steeper :: Bump -> Bump -> Bump -> Bool
- Game.LambdaHack.FOV.Common: type ConvexHull = [Bump]
- Game.LambdaHack.FOV.Common: type Distance = Int
- Game.LambdaHack.FOV.Common: type Edge = (Line, ConvexHull)
- Game.LambdaHack.FOV.Common: type EdgeInterval = (Edge, Edge)
- Game.LambdaHack.FOV.Common: type Line = (Bump, Bump)
- Game.LambdaHack.FOV.Common: type Progress = Int
- Game.LambdaHack.FOV.Digital: debugLine :: Line -> (Bool, String)
- Game.LambdaHack.FOV.Digital: debugSteeper :: Bump -> Bump -> Bump -> Bool
- Game.LambdaHack.FOV.Digital: dline :: Bump -> Bump -> Line
- Game.LambdaHack.FOV.Digital: dsteeper :: Bump -> Bump -> Bump -> Bool
- Game.LambdaHack.FOV.Digital: intersect :: Line -> Distance -> (Int, Int)
- Game.LambdaHack.FOV.Digital: scan :: Distance -> (Bump -> Bool) -> [Bump]
- Game.LambdaHack.FOV.Permissive: debugLine :: Line -> (Bool, String)
- Game.LambdaHack.FOV.Permissive: debugSteeper :: Bump -> Bump -> Bump -> Bool
- Game.LambdaHack.FOV.Permissive: dline :: Bump -> Bump -> Line
- Game.LambdaHack.FOV.Permissive: dsteeper :: Bump -> Bump -> Bump -> Bool
- Game.LambdaHack.FOV.Permissive: intersect :: Line -> Distance -> (Int, Int)
- Game.LambdaHack.FOV.Permissive: scan :: (Bump -> Bool) -> [Bump]
- Game.LambdaHack.FOV.Shadow: scan :: (SBump -> Bool) -> Distance -> Interval -> [SBump]
- Game.LambdaHack.FOV.Shadow: type Interval = (Rational, Rational)
- Game.LambdaHack.FOV.Shadow: type SBump = (Progress, Distance)
- Game.LambdaHack.Feature: Ascendable :: Feature
- Game.LambdaHack.Feature: Aura :: !Effect -> Feature
- Game.LambdaHack.Feature: Boring :: Feature
- Game.LambdaHack.Feature: Cause :: !Effect -> Feature
- Game.LambdaHack.Feature: ChangeTo :: !Text -> Feature
- Game.LambdaHack.Feature: Clear :: Feature
- Game.LambdaHack.Feature: Closable :: Feature
- Game.LambdaHack.Feature: Descendable :: Feature
- Game.LambdaHack.Feature: Exit :: Feature
- Game.LambdaHack.Feature: Hidden :: Feature
- Game.LambdaHack.Feature: Lit :: Feature
- Game.LambdaHack.Feature: Openable :: Feature
- Game.LambdaHack.Feature: Path :: Feature
- Game.LambdaHack.Feature: Secret :: !RollDice -> Feature
- Game.LambdaHack.Feature: Walkable :: Feature
- Game.LambdaHack.Feature: data Feature
- Game.LambdaHack.Feature: instance Eq Feature
- Game.LambdaHack.Feature: instance Ord Feature
- Game.LambdaHack.Feature: instance Read Feature
- Game.LambdaHack.Feature: instance Show Feature
- Game.LambdaHack.Flavour: brightCol :: [Color]
- Game.LambdaHack.Flavour: darkCol :: [Color]
- Game.LambdaHack.Flavour: data Flavour
- Game.LambdaHack.Flavour: flavourToColor :: Flavour -> Color
- Game.LambdaHack.Flavour: flavourToName :: Flavour -> Text
- Game.LambdaHack.Flavour: instance Binary Flavour
- Game.LambdaHack.Flavour: instance Eq Flavour
- Game.LambdaHack.Flavour: instance Ord Flavour
- Game.LambdaHack.Flavour: instance Show Flavour
- Game.LambdaHack.Flavour: stdCol :: [Color]
- Game.LambdaHack.Flavour: stdFlav :: [Flavour]
- Game.LambdaHack.Flavour: zipFancy :: [Color] -> [Flavour]
- Game.LambdaHack.Flavour: zipPlain :: [Color] -> [Flavour]
- Game.LambdaHack.Item: Item :: !(Id ItemKind) -> !Int -> Maybe Char -> !Int -> Item
- Game.LambdaHack.Item: assignLetter :: Maybe Char -> Char -> [Item] -> Maybe Char
- Game.LambdaHack.Item: cmpLetterMaybe :: Maybe Char -> Maybe Char -> Ordering
- Game.LambdaHack.Item: data Item
- Game.LambdaHack.Item: dungeonFlavourMap :: Ops ItemKind -> Rnd FlavourMap
- Game.LambdaHack.Item: equalItemIdentity :: Item -> Item -> Bool
- Game.LambdaHack.Item: getFlavour :: Ops ItemKind -> FlavourMap -> Id ItemKind -> Flavour
- Game.LambdaHack.Item: instance Binary Item
- Game.LambdaHack.Item: instance Show Item
- Game.LambdaHack.Item: itemPrice :: Ops ItemKind -> Item -> Int
- Game.LambdaHack.Item: jcount :: Item -> !Int
- Game.LambdaHack.Item: jkind :: Item -> !(Id ItemKind)
- Game.LambdaHack.Item: jletter :: Item -> Maybe Char
- Game.LambdaHack.Item: joinItem :: Item -> [Item] -> (Item, [Item])
- Game.LambdaHack.Item: jpower :: Item -> !Int
- Game.LambdaHack.Item: letterLabel :: Maybe Char -> Part
- Game.LambdaHack.Item: letterRange :: [Char] -> Text
- Game.LambdaHack.Item: maxLetter :: Char -> Char -> Char
- Game.LambdaHack.Item: newItem :: Ops ItemKind -> Int -> Int -> Rnd Item
- Game.LambdaHack.Item: removeItemByIdentity :: Item -> [Item] -> [Item]
- Game.LambdaHack.Item: removeItemByLetter :: Item -> [Item] -> [Item]
- Game.LambdaHack.Item: strongestRegen :: Ops ItemKind -> [Item] -> Maybe Item
- Game.LambdaHack.Item: strongestSearch :: Ops ItemKind -> [Item] -> Maybe Item
- Game.LambdaHack.Item: strongestSword :: COps -> [Item] -> Maybe Item
- Game.LambdaHack.Item: type Discoveries = Set (Id ItemKind)
- Game.LambdaHack.Item: type FlavourMap = Map (Id ItemKind) Flavour
- Game.LambdaHack.Item: viewItem :: Ops ItemKind -> Id ItemKind -> FlavourMap -> (Char, Color)
- Game.LambdaHack.ItemAction: IAll :: ItemDialogState
- Game.LambdaHack.ItemAction: INone :: ItemDialogState
- Game.LambdaHack.ItemAction: ISuitable :: ItemDialogState
- Game.LambdaHack.ItemAction: acceptCurrent :: ActionFrame () -> ActionFrame ()
- Game.LambdaHack.ItemAction: actorPickupItem :: ActorId -> Action ()
- Game.LambdaHack.ItemAction: allObjectsName :: Text
- Game.LambdaHack.ItemAction: applyGroupItem :: ActorId -> Part -> Item -> Action ()
- Game.LambdaHack.ItemAction: cancelCurrent :: ActionFrame () -> ActionFrame ()
- Game.LambdaHack.ItemAction: clearCurrent :: Action ()
- Game.LambdaHack.ItemAction: data ItemDialogState
- Game.LambdaHack.ItemAction: dropItem :: Action ()
- Game.LambdaHack.ItemAction: endTargeting :: Bool -> Action ()
- Game.LambdaHack.ItemAction: endTargetingMsg :: Action ()
- Game.LambdaHack.ItemAction: epsIncr :: Bool -> Action ()
- Game.LambdaHack.ItemAction: getAnyItem :: Text -> [Item] -> Text -> Action Item
- Game.LambdaHack.ItemAction: getGroupItem :: [Item] -> Part -> [Char] -> Text -> Text -> Action Item
- Game.LambdaHack.ItemAction: getItem :: Text -> (Item -> Bool) -> Text -> [Item] -> Text -> Action Item
- Game.LambdaHack.ItemAction: instance Eq ItemDialogState
- Game.LambdaHack.ItemAction: inventory :: ActionFrame ()
- Game.LambdaHack.ItemAction: pickupItem :: Action ()
- Game.LambdaHack.ItemAction: playerApplyGroupItem :: Part -> Part -> [Char] -> Action ()
- Game.LambdaHack.ItemAction: playerProjectGI :: Part -> Part -> [Char] -> ActionFrame ()
- Game.LambdaHack.ItemAction: playerProjectGroupItem :: Part -> Part -> [Char] -> ActionFrame ()
- Game.LambdaHack.ItemAction: projectGroupItem :: ActorId -> Point -> Part -> Item -> Action ()
- Game.LambdaHack.ItemAction: removeFromInventory :: ActorId -> Item -> Point -> Action ()
- Game.LambdaHack.ItemAction: removeFromLoc :: Item -> Point -> Action Bool
- Game.LambdaHack.ItemAction: removeOnlyFromInventory :: ActorId -> Item -> Point -> Action ()
- Game.LambdaHack.ItemAction: setCursor :: TgtMode -> ActionFrame ()
- Game.LambdaHack.ItemAction: targetFloor :: TgtMode -> ActionFrame ()
- Game.LambdaHack.ItemAction: targetMonster :: TgtMode -> ActionFrame ()
- Game.LambdaHack.Key: BackTab :: Key
- Game.LambdaHack.Key: Begin :: Key
- Game.LambdaHack.Key: Char :: !Char -> Key
- Game.LambdaHack.Key: Control :: Modifier
- Game.LambdaHack.Key: Down :: Key
- Game.LambdaHack.Key: End :: Key
- Game.LambdaHack.Key: Esc :: Key
- Game.LambdaHack.Key: Home :: Key
- Game.LambdaHack.Key: KP :: !Char -> Key
- Game.LambdaHack.Key: Left :: Key
- Game.LambdaHack.Key: NoModifier :: Modifier
- Game.LambdaHack.Key: PgDn :: Key
- Game.LambdaHack.Key: PgUp :: Key
- Game.LambdaHack.Key: Return :: Key
- Game.LambdaHack.Key: Right :: Key
- Game.LambdaHack.Key: Space :: Key
- Game.LambdaHack.Key: Tab :: Key
- Game.LambdaHack.Key: Unknown :: !String -> Key
- Game.LambdaHack.Key: Up :: Key
- Game.LambdaHack.Key: data Key
- Game.LambdaHack.Key: data Modifier
- Game.LambdaHack.Key: dirAllMoveKey :: [Key]
- Game.LambdaHack.Key: handleDir :: X -> (Key, Modifier) -> (Vector -> a) -> a -> a
- Game.LambdaHack.Key: instance Binary Key
- Game.LambdaHack.Key: instance Eq Key
- Game.LambdaHack.Key: instance Eq Modifier
- Game.LambdaHack.Key: instance Ord Key
- Game.LambdaHack.Key: instance Ord Modifier
- Game.LambdaHack.Key: instance Show Key
- Game.LambdaHack.Key: keyTranslate :: String -> Key
- Game.LambdaHack.Key: moveBinding :: ((X -> Vector) -> a) -> ((X -> Vector) -> a) -> [((Key, Modifier), (Text, Bool, a))]
- Game.LambdaHack.Key: showKM :: (Key, Modifier) -> Text
- Game.LambdaHack.Kind: (!) :: Ix i => Array i c -> i -> Id c
- Game.LambdaHack.Kind: (//) :: Ix i => Array i c -> [(i, Id c)] -> Array i c
- Game.LambdaHack.Kind: COps :: !(Ops ActorKind) -> !(Ops CaveKind) -> !(Ops FactionKind) -> !(Ops ItemKind) -> !(Ops PlaceKind) -> !(Ops RuleKind) -> !(Ops StrategyKind) -> !(Ops TileKind) -> COps
- Game.LambdaHack.Kind: Ops :: (Id a -> Char) -> (Id a -> Text) -> (Id a -> a) -> (Text -> Id a) -> (Text -> (a -> Bool) -> Rnd (Id a)) -> (forall b. (Id a -> a -> b -> b) -> b -> b) -> (Id a, Id a) -> Speedup a -> Ops a
- Game.LambdaHack.Kind: array :: Ix i => (i, i) -> [(i, Id c)] -> Array i c
- Game.LambdaHack.Kind: bounds :: Ix i => Array i c -> (i, i)
- Game.LambdaHack.Kind: coactor :: COps -> !(Ops ActorKind)
- Game.LambdaHack.Kind: cocave :: COps -> !(Ops CaveKind)
- Game.LambdaHack.Kind: cofact :: COps -> !(Ops FactionKind)
- Game.LambdaHack.Kind: coitem :: COps -> !(Ops ItemKind)
- Game.LambdaHack.Kind: coplace :: COps -> !(Ops PlaceKind)
- Game.LambdaHack.Kind: corule :: COps -> !(Ops RuleKind)
- Game.LambdaHack.Kind: costrat :: COps -> !(Ops StrategyKind)
- Game.LambdaHack.Kind: cotile :: COps -> !(Ops TileKind)
- Game.LambdaHack.Kind: createOps :: Show a => CDefs a -> Ops a
- Game.LambdaHack.Kind: data Array i c
- Game.LambdaHack.Kind: data COps
- Game.LambdaHack.Kind: data Id c
- Game.LambdaHack.Kind: data Ops a
- Game.LambdaHack.Kind: foldlArray :: Ix i => (a -> Id c -> a) -> a -> Array i c -> a
- Game.LambdaHack.Kind: instance (Ix i, Binary i) => Binary (Array i c)
- Game.LambdaHack.Kind: instance (Show i, Ix i) => Show (Array i c)
- Game.LambdaHack.Kind: instance Binary (Id c)
- Game.LambdaHack.Kind: instance Eq (Id c)
- Game.LambdaHack.Kind: instance Ix (Id c)
- Game.LambdaHack.Kind: instance Ord (Id c)
- Game.LambdaHack.Kind: instance Show (Id c)
- Game.LambdaHack.Kind: instance Show COps
- Game.LambdaHack.Kind: listArray :: Ix i => (i, i) -> [Id c] -> Array i c
- Game.LambdaHack.Kind: obounds :: Ops a -> (Id a, Id a)
- Game.LambdaHack.Kind: ofoldrWithKey :: Ops a -> forall b. (Id a -> a -> b -> b) -> b -> b
- Game.LambdaHack.Kind: okind :: Ops a -> Id a -> a
- Game.LambdaHack.Kind: oname :: Ops a -> Id a -> Text
- Game.LambdaHack.Kind: opick :: Ops a -> Text -> (a -> Bool) -> Rnd (Id a)
- Game.LambdaHack.Kind: ospeedup :: Ops a -> Speedup a
- Game.LambdaHack.Kind: osymbol :: Ops a -> Id a -> Char
- Game.LambdaHack.Kind: ouniqGroup :: Ops a -> Text -> Id a
- Game.LambdaHack.Kind: stdRuleset :: Ops RuleKind -> RuleKind
- Game.LambdaHack.Level: Level :: ActorDict -> InvDict -> X -> Y -> SmellMap -> SecretMap -> ItemMap -> TileMap -> TileMap -> Text -> Text -> (Point, Point) -> Time -> Int -> Int -> Level
- Game.LambdaHack.Level: accessible :: COps -> Level -> Point -> Point -> Bool
- Game.LambdaHack.Level: at :: Level -> Point -> Id TileKind
- Game.LambdaHack.Level: atI :: Level -> Point -> [Item]
- Game.LambdaHack.Level: data Level
- Game.LambdaHack.Level: dropItemsAt :: [Item] -> Point -> Level -> Level
- Game.LambdaHack.Level: findLoc :: TileMap -> (Point -> Id TileKind -> Bool) -> Rnd Point
- Game.LambdaHack.Level: findLocTry :: Int -> TileMap -> [Point -> Id TileKind -> Bool] -> Rnd Point
- Game.LambdaHack.Level: instance Binary Level
- Game.LambdaHack.Level: instance Show Level
- Game.LambdaHack.Level: lactor :: Level -> ActorDict
- Game.LambdaHack.Level: lclear :: Level -> Int
- Game.LambdaHack.Level: ldesc :: Level -> Text
- Game.LambdaHack.Level: linv :: Level -> InvDict
- Game.LambdaHack.Level: litem :: Level -> ItemMap
- Game.LambdaHack.Level: lmap :: Level -> TileMap
- Game.LambdaHack.Level: lmeta :: Level -> Text
- Game.LambdaHack.Level: lrmap :: Level -> TileMap
- Game.LambdaHack.Level: lsecret :: Level -> SecretMap
- Game.LambdaHack.Level: lseen :: Level -> Int
- Game.LambdaHack.Level: lsmell :: Level -> SmellMap
- Game.LambdaHack.Level: lstairs :: Level -> (Point, Point)
- Game.LambdaHack.Level: ltime :: Level -> Time
- Game.LambdaHack.Level: lxsize :: Level -> X
- Game.LambdaHack.Level: lysize :: Level -> Y
- Game.LambdaHack.Level: openable :: Ops TileKind -> Level -> SecretTime -> Point -> Bool
- Game.LambdaHack.Level: rememberAt :: Level -> Point -> Id TileKind
- Game.LambdaHack.Level: rememberAtI :: Level -> Point -> [Item]
- Game.LambdaHack.Level: type ActorDict = IntMap Actor
- Game.LambdaHack.Level: type InvDict = IntMap [Item]
- Game.LambdaHack.Level: type ItemMap = IntMap ([Item], [Item])
- Game.LambdaHack.Level: type SecretMap = IntMap SecretTime
- Game.LambdaHack.Level: type SmellMap = IntMap SmellTime
- Game.LambdaHack.Level: type TileMap = Array Point TileKind
- Game.LambdaHack.Level: updateActorDict :: (ActorDict -> ActorDict) -> Level -> Level
- Game.LambdaHack.Level: updateIMap :: (ItemMap -> ItemMap) -> Level -> Level
- Game.LambdaHack.Level: updateInv :: (InvDict -> InvDict) -> Level -> Level
- Game.LambdaHack.Level: updateLMap :: (TileMap -> TileMap) -> Level -> Level
- Game.LambdaHack.Level: updateLRMap :: (TileMap -> TileMap) -> Level -> Level
- Game.LambdaHack.Level: updateSmell :: (SmellMap -> SmellMap) -> Level -> Level
- Game.LambdaHack.Misc: breturn :: MonadPlus m => Bool -> a -> m a
- Game.LambdaHack.Misc: divUp :: Int -> Int -> Int
- Game.LambdaHack.Misc: normalLevelBound :: (Int, Int)
- Game.LambdaHack.Misc: type Freqs = [(Text, Int)]
- Game.LambdaHack.Msg: (<+>) :: Text -> Text -> Text
- Game.LambdaHack.Msg: (<>) :: Text -> Text -> Text
- Game.LambdaHack.Msg: addMsg :: Report -> Msg -> Report
- Game.LambdaHack.Msg: addReport :: Report -> History -> History
- Game.LambdaHack.Msg: data History
- Game.LambdaHack.Msg: data Report
- Game.LambdaHack.Msg: emptyHistory :: History
- Game.LambdaHack.Msg: emptyReport :: Report
- Game.LambdaHack.Msg: instance Binary History
- Game.LambdaHack.Msg: instance Binary Report
- Game.LambdaHack.Msg: instance Binary Text
- Game.LambdaHack.Msg: instance Show History
- Game.LambdaHack.Msg: instance Show Report
- Game.LambdaHack.Msg: makePhrase :: [Part] -> Text
- Game.LambdaHack.Msg: makeSentence :: [Part] -> Text
- Game.LambdaHack.Msg: moreMsg :: Msg
- Game.LambdaHack.Msg: nullReport :: Report -> Bool
- Game.LambdaHack.Msg: padMsg :: X -> Text -> Text
- Game.LambdaHack.Msg: renderHistory :: History -> Overlay
- Game.LambdaHack.Msg: renderReport :: Report -> Text
- Game.LambdaHack.Msg: showT :: Show a => a -> Text
- Game.LambdaHack.Msg: singletonHistory :: Report -> History
- Game.LambdaHack.Msg: singletonReport :: Msg -> Report
- Game.LambdaHack.Msg: splitOverlay :: Y -> Overlay -> [Overlay]
- Game.LambdaHack.Msg: splitReport :: Report -> [Text]
- Game.LambdaHack.Msg: stringByLocation :: X -> Y -> Overlay -> (Text, PointXY -> Maybe Char, Maybe Text)
- Game.LambdaHack.Msg: takeHistory :: Int -> History -> History
- Game.LambdaHack.Msg: type Msg = Text
- Game.LambdaHack.Msg: type Overlay = [Text]
- Game.LambdaHack.Msg: yesnoMsg :: Msg
- Game.LambdaHack.Perception: actorReachesActor :: ActorId -> ActorId -> Point -> Point -> Perception -> Maybe ActorId -> Bool
- Game.LambdaHack.Perception: actorReachesLoc :: ActorId -> Point -> Perception -> Maybe ActorId -> Bool
- Game.LambdaHack.Perception: actorSeesActor :: Ops TileKind -> Perception -> Level -> ActorId -> ActorId -> Point -> Point -> ActorId -> Bool
- Game.LambdaHack.Perception: data Perception
- Game.LambdaHack.Perception: debugTotalReachable :: Perception -> IntSet
- Game.LambdaHack.Perception: dungeonPerception :: COps -> State -> DungeonPerception
- Game.LambdaHack.Perception: totalVisible :: Perception -> IntSet
- Game.LambdaHack.Perception: type DungeonPerception = [(LevelId, Perception)]
- Game.LambdaHack.Place: Place :: !(Id PlaceKind) -> !Area -> !Bool -> !Text -> !(Id TileKind) -> !(Id TileKind) -> Place
- Game.LambdaHack.Place: buildFence :: Id TileKind -> Area -> TileMapXY
- Game.LambdaHack.Place: buildPlace :: COps -> CaveKind -> Id TileKind -> Int -> Int -> Area -> Rnd (TileMapXY, Place)
- Game.LambdaHack.Place: data Place
- Game.LambdaHack.Place: instance Binary Place
- Game.LambdaHack.Place: instance Show Place
- Game.LambdaHack.Place: placeValid :: Area -> PlaceKind -> Bool
- Game.LambdaHack.Place: qarea :: Place -> !Area
- Game.LambdaHack.Place: qhollowFence :: Place -> !(Id TileKind)
- Game.LambdaHack.Place: qkind :: Place -> !(Id PlaceKind)
- Game.LambdaHack.Place: qlegend :: Place -> !Text
- Game.LambdaHack.Place: qseen :: Place -> !Bool
- Game.LambdaHack.Place: qsolidFence :: Place -> !(Id TileKind)
- Game.LambdaHack.Place: type TileMapXY = Map PointXY (Id TileKind)
- Game.LambdaHack.Point: adjacent :: X -> Point -> Point -> Bool
- Game.LambdaHack.Point: bla :: X -> Y -> Int -> Point -> Point -> Maybe [Point]
- Game.LambdaHack.Point: chessDist :: X -> Point -> Point -> Int
- Game.LambdaHack.Point: displacementXYZ :: X -> Point -> Point -> VectorXY
- Game.LambdaHack.Point: inside :: X -> Point -> Area -> Bool
- Game.LambdaHack.Point: origin :: Point
- Game.LambdaHack.Point: showPoint :: X -> Point -> Text
- Game.LambdaHack.Point: toPoint :: X -> PointXY -> Point
- Game.LambdaHack.Point: type Point = Int
- Game.LambdaHack.Point: vicinity :: X -> Y -> Point -> [Point]
- Game.LambdaHack.Point: vicinityCardinal :: X -> Y -> Point -> [Point]
- Game.LambdaHack.PointXY: PointXY :: (X, Y) -> PointXY
- Game.LambdaHack.PointXY: blaXY :: Int -> PointXY -> PointXY -> [PointXY]
- Game.LambdaHack.PointXY: fromTo :: PointXY -> PointXY -> [PointXY]
- Game.LambdaHack.PointXY: instance Eq PointXY
- Game.LambdaHack.PointXY: instance Ord PointXY
- Game.LambdaHack.PointXY: instance Show PointXY
- Game.LambdaHack.PointXY: newtype PointXY
- Game.LambdaHack.PointXY: sortPointXY :: (PointXY, PointXY) -> (PointXY, PointXY)
- Game.LambdaHack.PointXY: type X = Int
- Game.LambdaHack.PointXY: type Y = Int
- Game.LambdaHack.Random: RollDice :: Word8 -> Word8 -> RollDice
- Game.LambdaHack.Random: RollDiceXY :: (RollDice, RollDice) -> RollDiceXY
- Game.LambdaHack.Random: chance :: Chance -> Rnd Bool
- Game.LambdaHack.Random: chanceDeep :: Int -> Int -> RollDeep -> Rnd Bool
- Game.LambdaHack.Random: data RollDice
- Game.LambdaHack.Random: data RollDiceXY
- Game.LambdaHack.Random: frequency :: Show a => Frequency a -> Rnd a
- Game.LambdaHack.Random: instance Eq RollDice
- Game.LambdaHack.Random: instance Ord RollDice
- Game.LambdaHack.Random: instance Read RollDice
- Game.LambdaHack.Random: instance Show RollDice
- Game.LambdaHack.Random: instance Show RollDiceXY
- Game.LambdaHack.Random: intToDeep :: Int -> RollDeep
- Game.LambdaHack.Random: maxDeep :: RollDeep -> Int
- Game.LambdaHack.Random: maxDice :: RollDice -> Int
- Game.LambdaHack.Random: meanDice :: RollDice -> Rational
- Game.LambdaHack.Random: minDice :: RollDice -> Int
- Game.LambdaHack.Random: oneOf :: [a] -> Rnd a
- Game.LambdaHack.Random: randomR :: Random a => (a, a) -> Rnd a
- Game.LambdaHack.Random: roll :: Int -> Rnd Int
- Game.LambdaHack.Random: rollDeep :: Int -> Int -> RollDeep -> Rnd Int
- Game.LambdaHack.Random: rollDice :: RollDice -> Rnd Int
- Game.LambdaHack.Random: rollDiceXY :: RollDiceXY -> Rnd (Int, Int)
- Game.LambdaHack.Random: type Chance = Rational
- Game.LambdaHack.Random: type Rnd a = State StdGen a
- Game.LambdaHack.Random: type RollDeep = (RollDice, RollDice)
- Game.LambdaHack.Running: continueRun :: (Vector, Int) -> Action ()
- Game.LambdaHack.Running: run :: (Vector, Int) -> ActionFrame ()
- Game.LambdaHack.State: Camping :: Status
- Game.LambdaHack.State: Cursor :: TgtMode -> LevelId -> Point -> LevelId -> Int -> Cursor
- Game.LambdaHack.State: DebugMode :: Maybe FovMode -> Bool -> DebugMode
- Game.LambdaHack.State: Diary :: Report -> History -> Diary
- Game.LambdaHack.State: Killed :: !LevelId -> Status
- Game.LambdaHack.State: Restart :: Status
- Game.LambdaHack.State: State :: ActorId -> Cursor -> FlavourMap -> Discoveries -> Dungeon -> LevelId -> Int -> StdGen -> Config -> Maybe Bool -> Maybe (Bool, Status) -> Id FactionKind -> DebugMode -> State
- Game.LambdaHack.State: TgtAuto :: TgtMode
- Game.LambdaHack.State: TgtExplicit :: TgtMode
- Game.LambdaHack.State: TgtOff :: TgtMode
- Game.LambdaHack.State: Victor :: Status
- Game.LambdaHack.State: ceps :: Cursor -> Int
- Game.LambdaHack.State: clocLn :: Cursor -> LevelId
- Game.LambdaHack.State: clocation :: Cursor -> Point
- Game.LambdaHack.State: creturnLn :: Cursor -> LevelId
- Game.LambdaHack.State: ctargeting :: Cursor -> TgtMode
- Game.LambdaHack.State: cycleMarkVision :: State -> State
- Game.LambdaHack.State: data Cursor
- Game.LambdaHack.State: data DebugMode
- Game.LambdaHack.State: data Diary
- Game.LambdaHack.State: data State
- Game.LambdaHack.State: data Status
- Game.LambdaHack.State: data TgtMode
- Game.LambdaHack.State: defaultDiary :: IO Diary
- Game.LambdaHack.State: defaultState :: Config -> Id FactionKind -> FlavourMap -> Dungeon -> LevelId -> Point -> StdGen -> State
- Game.LambdaHack.State: instance Binary Cursor
- Game.LambdaHack.State: instance Binary Diary
- Game.LambdaHack.State: instance Binary State
- Game.LambdaHack.State: instance Binary Status
- Game.LambdaHack.State: instance Binary TgtMode
- Game.LambdaHack.State: instance Eq Status
- Game.LambdaHack.State: instance Eq TgtMode
- Game.LambdaHack.State: instance Ord Status
- Game.LambdaHack.State: instance Show Cursor
- Game.LambdaHack.State: instance Show DebugMode
- Game.LambdaHack.State: instance Show State
- Game.LambdaHack.State: instance Show Status
- Game.LambdaHack.State: instance Show TgtMode
- Game.LambdaHack.State: lookAt :: COps -> Bool -> Bool -> State -> Level -> Point -> Text -> Text
- Game.LambdaHack.State: partItem :: Ops ItemKind -> State -> Item -> Part
- Game.LambdaHack.State: partItemCheat :: Bool -> Ops ItemKind -> State -> Item -> Part
- Game.LambdaHack.State: partItemNWs :: Ops ItemKind -> State -> Item -> Part
- Game.LambdaHack.State: sconfig :: State -> Config
- Game.LambdaHack.State: scounter :: State -> Int
- Game.LambdaHack.State: scursor :: State -> Cursor
- Game.LambdaHack.State: sdebug :: State -> DebugMode
- Game.LambdaHack.State: sdisco :: State -> Discoveries
- Game.LambdaHack.State: sdungeon :: State -> Dungeon
- Game.LambdaHack.State: sfaction :: State -> Id FactionKind
- Game.LambdaHack.State: sflavour :: State -> FlavourMap
- Game.LambdaHack.State: shistory :: Diary -> History
- Game.LambdaHack.State: slevel :: State -> Level
- Game.LambdaHack.State: slid :: State -> LevelId
- Game.LambdaHack.State: smarkVision :: DebugMode -> Maybe FovMode
- Game.LambdaHack.State: somniscient :: DebugMode -> Bool
- Game.LambdaHack.State: splayer :: State -> ActorId
- Game.LambdaHack.State: squit :: State -> Maybe (Bool, Status)
- Game.LambdaHack.State: srandom :: State -> StdGen
- Game.LambdaHack.State: sreport :: Diary -> Report
- Game.LambdaHack.State: stakeTime :: State -> Maybe Bool
- Game.LambdaHack.State: stime :: State -> Time
- Game.LambdaHack.State: toggleOmniscient :: State -> State
- Game.LambdaHack.State: updateCursor :: (Cursor -> Cursor) -> State -> State
- Game.LambdaHack.State: updateDiscoveries :: (Discoveries -> Discoveries) -> State -> State
- Game.LambdaHack.State: updateDungeon :: (Dungeon -> Dungeon) -> State -> State
- Game.LambdaHack.State: updateLevel :: (Level -> Level) -> State -> State
- Game.LambdaHack.State: updateTime :: (Time -> Time) -> State -> State
- Game.LambdaHack.Strategy: (.=>) :: Bool -> Strategy a -> Strategy a
- Game.LambdaHack.Strategy: (.|) :: Strategy a -> Strategy a -> Strategy a
- Game.LambdaHack.Strategy: bestVariant :: Strategy a -> Frequency a
- Game.LambdaHack.Strategy: data Strategy a
- Game.LambdaHack.Strategy: instance Monad Strategy
- Game.LambdaHack.Strategy: instance MonadPlus Strategy
- Game.LambdaHack.Strategy: instance Show a => Show (Strategy a)
- Game.LambdaHack.Strategy: liftFrequency :: Frequency a -> Strategy a
- Game.LambdaHack.Strategy: nullStrategy :: Strategy a -> Bool
- Game.LambdaHack.Strategy: only :: (a -> Bool) -> Strategy a -> Strategy a
- Game.LambdaHack.Strategy: reject :: Strategy a
- Game.LambdaHack.Strategy: renameStrategy :: Text -> Strategy a -> Strategy a
- Game.LambdaHack.Strategy: returN :: Text -> a -> Strategy a
- Game.LambdaHack.StrategyAction: strategy :: COps -> ActorId -> State -> [Ability] -> Strategy (Action ())
- Game.LambdaHack.StrategyAction: targetStrategy :: COps -> ActorId -> State -> Perception -> [Ability] -> Strategy Target
- Game.LambdaHack.Tile: canBeHidden :: Ops TileKind -> TileKind -> Bool
- Game.LambdaHack.Tile: hasFeature :: Ops TileKind -> Feature -> Id TileKind -> Bool
- Game.LambdaHack.Tile: isClear :: Ops TileKind -> Id TileKind -> Bool
- Game.LambdaHack.Tile: isExplorable :: Ops TileKind -> Id TileKind -> Bool
- Game.LambdaHack.Tile: isLit :: Ops TileKind -> Id TileKind -> Bool
- Game.LambdaHack.Tile: kindHas :: [Feature] -> [Feature] -> TileKind -> Bool
- Game.LambdaHack.Tile: kindHasFeature :: Feature -> TileKind -> Bool
- Game.LambdaHack.Tile: similar :: TileKind -> TileKind -> Bool
- Game.LambdaHack.Tile: speedup :: Ops TileKind -> Speedup TileKind
- Game.LambdaHack.Tile: type SecretTime = Time
- Game.LambdaHack.Tile: type SmellTime = Time
- Game.LambdaHack.Time: data Speed
- Game.LambdaHack.Time: data Time
- Game.LambdaHack.Time: instance Binary Speed
- Game.LambdaHack.Time: instance Binary Time
- Game.LambdaHack.Time: instance Eq Speed
- Game.LambdaHack.Time: instance Eq Time
- Game.LambdaHack.Time: instance Ord Speed
- Game.LambdaHack.Time: instance Ord Time
- Game.LambdaHack.Time: instance Show Speed
- Game.LambdaHack.Time: instance Show Time
- Game.LambdaHack.Time: rangeFromSpeed :: Speed -> Int
- Game.LambdaHack.Time: speedFromWeight :: Int -> Int -> Speed
- Game.LambdaHack.Time: speedNormal :: Speed
- Game.LambdaHack.Time: speedScale :: Speed -> Int -> Speed
- Game.LambdaHack.Time: ticksPerMeter :: Speed -> Time
- Game.LambdaHack.Time: timeAdd :: Time -> Time -> Time
- Game.LambdaHack.Time: timeClip :: Time
- Game.LambdaHack.Time: timeFit :: Time -> Time -> Int
- Game.LambdaHack.Time: timeNegate :: Time -> Time
- Game.LambdaHack.Time: timeScale :: Time -> Int -> Time
- Game.LambdaHack.Time: timeToDigit :: Time -> Time -> Char
- Game.LambdaHack.Time: timeTurn :: Time
- Game.LambdaHack.Time: timeZero :: Time
- Game.LambdaHack.Time: toSpeed :: Double -> Speed
- Game.LambdaHack.Time: traveled :: Speed -> Time -> Int
- Game.LambdaHack.Turn: handleTurn :: Action ()
- Game.LambdaHack.Utils.Assert: checkM :: (Show a, Monad m) => (Bool -> m () -> m ()) -> (c -> Bool) -> a -> c -> m ()
- Game.LambdaHack.Utils.Assert: falseM :: (Show a, Monad m) => (Bool -> m () -> m ()) -> a -> Bool -> m ()
- Game.LambdaHack.Utils.Assert: trueM :: (Show a, Monad m) => (Bool -> m () -> m ()) -> a -> Bool -> m ()
- Game.LambdaHack.Vector: data Vector
- Game.LambdaHack.Vector: diagonal :: X -> Vector -> Bool
- Game.LambdaHack.Vector: displacePath :: [Point] -> [Vector]
- Game.LambdaHack.Vector: displacement :: Point -> Point -> Vector
- Game.LambdaHack.Vector: euclidDistSq :: X -> Vector -> Vector -> Int
- Game.LambdaHack.Vector: instance Binary Vector
- Game.LambdaHack.Vector: instance Eq Vector
- Game.LambdaHack.Vector: instance Show Vector
- Game.LambdaHack.Vector: isUnit :: X -> Vector -> Bool
- Game.LambdaHack.Vector: moves :: X -> [Vector]
- Game.LambdaHack.Vector: movesWidth :: [X -> Vector]
- Game.LambdaHack.Vector: neg :: Vector -> Vector
- Game.LambdaHack.Vector: shift :: Point -> Vector -> Point
- Game.LambdaHack.Vector: shiftBounded :: X -> Area -> Point -> Vector -> Point
- Game.LambdaHack.Vector: shiftPath :: Point -> [Vector] -> [Point]
- Game.LambdaHack.Vector: toVector :: X -> VectorXY -> Vector
- Game.LambdaHack.Vector: towards :: X -> Point -> Point -> Vector
- Game.LambdaHack.VectorXY: VectorXY :: (X, Y) -> VectorXY
- Game.LambdaHack.VectorXY: chessDistXY :: VectorXY -> Int
- Game.LambdaHack.VectorXY: euclidDistSqXY :: VectorXY -> Int
- Game.LambdaHack.VectorXY: instance Eq VectorXY
- Game.LambdaHack.VectorXY: instance Show VectorXY
- Game.LambdaHack.VectorXY: movesCardinalXY :: [VectorXY]
- Game.LambdaHack.VectorXY: movesXY :: [VectorXY]
- Game.LambdaHack.VectorXY: negXY :: VectorXY -> VectorXY
- Game.LambdaHack.VectorXY: newtype VectorXY
- Game.LambdaHack.VectorXY: shiftXY :: PointXY -> VectorXY -> PointXY
+ Game.LambdaHack.Client: class MonadActionRO m => MonadClient m
+ Game.LambdaHack.Client: class MonadClient m => MonadClientUI m
+ Game.LambdaHack.Client: class MonadClient m => MonadConnClient c m | m -> c
+ Game.LambdaHack.Client: cmdClientAISem :: (MonadAtomic m, MonadConnClient c m) => CmdClientAI -> m ()
+ Game.LambdaHack.Client: cmdClientUISem :: (MonadAtomic m, MonadClientAbort m, MonadClientUI m, MonadConnClient c m) => CmdClientUI -> m ()
+ Game.LambdaHack.Client: exeFrontend :: (MonadAtomic m, MonadClientAbort m, MonadClientUI m, MonadConnClient CmdClientUI m, MonadAtomic n, MonadConnClient CmdClientAI n) => (m () -> SessionUI -> State -> StateClient -> ConnServer CmdClientUI -> IO ()) -> (n () -> SessionUI -> State -> StateClient -> ConnServer CmdClientAI -> IO ()) -> COps -> ((FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ()) -> (FactionId -> ChanServer CmdClientAI -> IO ()) -> IO ()) -> IO ()
+ Game.LambdaHack.Client: loopAI :: MonadConnClient CmdClientAI m => (CmdClientAI -> m ()) -> m ()
+ Game.LambdaHack.Client: loopUI :: (MonadClientUI m, MonadConnClient CmdClientUI m) => (CmdClientUI -> m ()) -> m ()
+ Game.LambdaHack.Client.Action: ConnFrontend :: (forall m. MonadClientUI m => m KM) -> (forall m. MonadClientUI m => FrontReq -> m ()) -> ConnFrontend
+ Game.LambdaHack.Client.Action: ConnServer :: (forall m. MonadConnClient c m => m c) -> (forall m. MonadConnClient c m => CmdSer -> m ()) -> ConnServer c
+ Game.LambdaHack.Client.Action: SessionUI :: !ConnFrontend -> !Binding -> SessionUI
+ Game.LambdaHack.Client.Action: abort :: MonadClientAbort m => m a
+ Game.LambdaHack.Client.Action: abortIfWith :: MonadClientAbort m => Bool -> Msg -> m a
+ Game.LambdaHack.Client.Action: abortWith :: MonadClientAbort m => Msg -> m a
+ Game.LambdaHack.Client.Action: animate :: MonadClientUI m => LevelId -> Animation -> m Frames
+ Game.LambdaHack.Client.Action: askBinding :: MonadClientUI m => m Binding
+ Game.LambdaHack.Client.Action: class MonadActionRO m => MonadClient m
+ Game.LambdaHack.Client.Action: class MonadClient m => MonadClientAbort m
+ Game.LambdaHack.Client.Action: class MonadClient m => MonadClientUI m
+ Game.LambdaHack.Client.Action: class MonadClient m => MonadConnClient c m | m -> c
+ Game.LambdaHack.Client.Action: clientGameSave :: MonadClient m => Bool -> m ()
+ Game.LambdaHack.Client.Action: connFrontend :: FactionId -> ChanFrontend -> ConnFrontend
+ Game.LambdaHack.Client.Action: connServer :: ChanServer c -> ConnServer c
+ Game.LambdaHack.Client.Action: data ConnFrontend
+ Game.LambdaHack.Client.Action: data ConnServer c
+ Game.LambdaHack.Client.Action: data SessionUI
+ Game.LambdaHack.Client.Action: debugPrint :: MonadClient m => Text -> m ()
+ Game.LambdaHack.Client.Action: displayChoiceUI :: (MonadClientAbort m, MonadClientUI m) => Msg -> Overlay -> [KM] -> m KM
+ Game.LambdaHack.Client.Action: displayFrames :: MonadClientUI m => Frames -> m ()
+ Game.LambdaHack.Client.Action: displayMore :: MonadClientUI m => ColorMode -> Msg -> m Bool
+ Game.LambdaHack.Client.Action: displayPush :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.Action: displayYesNo :: MonadClientUI m => ColorMode -> Msg -> m Bool
+ Game.LambdaHack.Client.Action: drawOverlay :: MonadClientUI m => ColorMode -> Overlay -> m SingleFrame
+ Game.LambdaHack.Client.Action: getArenaUI :: MonadClientUI m => m LevelId
+ Game.LambdaHack.Client.Action: getClient :: MonadClient m => m StateClient
+ Game.LambdaHack.Client.Action: getConn :: MonadConnClient c m => m (ConnServer c)
+ Game.LambdaHack.Client.Action: getInitConfirms :: MonadClientUI m => [KM] -> Slideshow -> m Bool
+ Game.LambdaHack.Client.Action: getKeyOverlayCommand :: MonadClientUI m => Overlay -> m KM
+ Game.LambdaHack.Client.Action: getLeaderUI :: MonadClientUI m => m ActorId
+ Game.LambdaHack.Client.Action: getPerFid :: MonadClient m => LevelId -> m Perception
+ Game.LambdaHack.Client.Action: getsClient :: MonadClient m => (StateClient -> a) -> m a
+ Game.LambdaHack.Client.Action: mkConfigUI :: Ops RuleKind -> IO ConfigUI
+ Game.LambdaHack.Client.Action: modifyClient :: MonadClient m => (StateClient -> StateClient) -> m ()
+ Game.LambdaHack.Client.Action: msgAdd :: MonadClientUI m => Msg -> m ()
+ Game.LambdaHack.Client.Action: msgReset :: MonadClient m => Msg -> m ()
+ Game.LambdaHack.Client.Action: neverMind :: MonadClientAbort m => Bool -> m a
+ Game.LambdaHack.Client.Action: overlayToSlideshow :: MonadClientUI m => Msg -> Overlay -> m Slideshow
+ Game.LambdaHack.Client.Action: partActorLeader :: MonadClient m => ActorId -> Actor -> m Part
+ Game.LambdaHack.Client.Action: partAidLeader :: MonadClient m => ActorId -> m Part
+ Game.LambdaHack.Client.Action: promptToSlideshow :: MonadClientUI m => Msg -> m Slideshow
+ Game.LambdaHack.Client.Action: putClient :: MonadClient m => StateClient -> m ()
+ Game.LambdaHack.Client.Action: readConnFrontend :: ConnFrontend -> forall m. MonadClientUI m => m KM
+ Game.LambdaHack.Client.Action: readConnServer :: ConnServer c -> forall m. MonadConnClient c m => m c
+ Game.LambdaHack.Client.Action: recordHistory :: MonadClient m => m ()
+ Game.LambdaHack.Client.Action: restoreGame :: MonadClient m => m (Either (State, StateClient, Msg) Msg)
+ Game.LambdaHack.Client.Action: rndToAction :: MonadClient m => Rnd a -> m a
+ Game.LambdaHack.Client.Action: sbinding :: SessionUI -> !Binding
+ Game.LambdaHack.Client.Action: scoreToSlideshow :: MonadClientUI m => Int -> Status -> m Slideshow
+ Game.LambdaHack.Client.Action: sfconn :: SessionUI -> !ConnFrontend
+ Game.LambdaHack.Client.Action: targetToPos :: MonadClientUI m => m (Maybe Point)
+ Game.LambdaHack.Client.Action: tryIgnore :: MonadClientAbort m => m () -> m ()
+ Game.LambdaHack.Client.Action: tryRepeatedlyWith :: MonadClientAbort m => (Msg -> m ()) -> m () -> m ()
+ Game.LambdaHack.Client.Action: tryWith :: MonadClientAbort m => (Msg -> m a) -> m a -> m a
+ Game.LambdaHack.Client.Action: tryWithSlide :: (MonadClientAbort m, MonadClientUI m) => m a -> WriterT Slideshow m a -> WriterT Slideshow m a
+ Game.LambdaHack.Client.Action: writeConnFrontend :: ConnFrontend -> forall m. MonadClientUI m => FrontReq -> m ()
+ Game.LambdaHack.Client.Action: writeConnServer :: ConnServer c -> forall m. MonadConnClient c m => CmdSer -> m ()
+ Game.LambdaHack.Client.Action.ActionClass: ConnFrontend :: (forall m. MonadClientUI m => m KM) -> (forall m. MonadClientUI m => FrontReq -> m ()) -> ConnFrontend
+ Game.LambdaHack.Client.Action.ActionClass: ConnServer :: (forall m. MonadConnClient c m => m c) -> (forall m. MonadConnClient c m => CmdSer -> m ()) -> ConnServer c
+ Game.LambdaHack.Client.Action.ActionClass: SessionUI :: !ConnFrontend -> !Binding -> SessionUI
+ Game.LambdaHack.Client.Action.ActionClass: abortWith :: MonadClientAbort m => Msg -> m a
+ Game.LambdaHack.Client.Action.ActionClass: class MonadActionRO m => MonadClient m
+ Game.LambdaHack.Client.Action.ActionClass: class MonadClient m => MonadClientAbort m
+ Game.LambdaHack.Client.Action.ActionClass: class MonadClient m => MonadClientUI m
+ Game.LambdaHack.Client.Action.ActionClass: class MonadClient m => MonadConnClient c m | m -> c
+ Game.LambdaHack.Client.Action.ActionClass: data ConnFrontend
+ Game.LambdaHack.Client.Action.ActionClass: data ConnServer c
+ Game.LambdaHack.Client.Action.ActionClass: data SessionUI
+ Game.LambdaHack.Client.Action.ActionClass: getClient :: MonadClient m => m StateClient
+ Game.LambdaHack.Client.Action.ActionClass: getConn :: MonadConnClient c m => m (ConnServer c)
+ Game.LambdaHack.Client.Action.ActionClass: getsClient :: MonadClient m => (StateClient -> a) -> m a
+ Game.LambdaHack.Client.Action.ActionClass: getsSession :: MonadClientUI m => (SessionUI -> a) -> m a
+ Game.LambdaHack.Client.Action.ActionClass: instance (Monoid a, MonadClient m) => MonadClient (WriterT a m)
+ Game.LambdaHack.Client.Action.ActionClass: instance (Monoid a, MonadClientAbort m) => MonadClientAbort (WriterT a m)
+ Game.LambdaHack.Client.Action.ActionClass: instance (Monoid a, MonadClientUI m) => MonadClientUI (WriterT a m)
+ Game.LambdaHack.Client.Action.ActionClass: liftIO :: MonadClient m => IO a -> m a
+ Game.LambdaHack.Client.Action.ActionClass: modifyClient :: MonadClient m => (StateClient -> StateClient) -> m ()
+ Game.LambdaHack.Client.Action.ActionClass: putClient :: MonadClient m => StateClient -> m ()
+ Game.LambdaHack.Client.Action.ActionClass: readConnFrontend :: ConnFrontend -> forall m. MonadClientUI m => m KM
+ Game.LambdaHack.Client.Action.ActionClass: readConnServer :: ConnServer c -> forall m. MonadConnClient c m => m c
+ Game.LambdaHack.Client.Action.ActionClass: sbinding :: SessionUI -> !Binding
+ Game.LambdaHack.Client.Action.ActionClass: sfconn :: SessionUI -> !ConnFrontend
+ Game.LambdaHack.Client.Action.ActionClass: tryWith :: MonadClientAbort m => (Msg -> m a) -> m a -> m a
+ Game.LambdaHack.Client.Action.ActionClass: writeConnFrontend :: ConnFrontend -> forall m. MonadClientUI m => FrontReq -> m ()
+ Game.LambdaHack.Client.Action.ActionClass: writeConnServer :: ConnServer c -> forall m. MonadConnClient c m => CmdSer -> m ()
+ Game.LambdaHack.Client.Action.ActionType: data ActionCli c a
+ Game.LambdaHack.Client.Action.ActionType: executorCli :: ActionCli c () -> SessionUI -> State -> StateClient -> ConnServer c -> IO ()
+ Game.LambdaHack.Client.Action.ActionType: instance Functor (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance Monad (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance MonadAction (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance MonadActionRO (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance MonadClient (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance MonadClientAbort (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance MonadClientUI (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance MonadConnClient c (ActionCli c)
+ Game.LambdaHack.Client.Action.ActionType: instance Show (ActionCli c a)
+ Game.LambdaHack.Client.Action.ActionType: type FunActionCli c a = SessionUI -> ConnServer c -> (State -> StateClient -> a -> IO ()) -> (StdGen -> Msg -> IO ()) -> State -> StateClient -> IO ()
+ Game.LambdaHack.Client.Action.ConfigIO: instance Show CP
+ Game.LambdaHack.Client.Action.ConfigIO: mkConfigUI :: Ops RuleKind -> IO ConfigUI
+ Game.LambdaHack.Client.Action.Save: restoreGameCli :: String -> ConfigUI -> (FilePath -> IO FilePath) -> Text -> IO (Either (State, StateClient, Msg) Msg)
+ Game.LambdaHack.Client.Action.Save: saveGameCli :: String -> Bool -> ConfigUI -> State -> StateClient -> IO ()
+ Game.LambdaHack.Client.AtomicSemCli: cmdAtomicFilterCli :: MonadClient m => CmdAtomic -> m [CmdAtomic]
+ Game.LambdaHack.Client.AtomicSemCli: cmdAtomicSem :: MonadAction m => CmdAtomic -> m ()
+ Game.LambdaHack.Client.AtomicSemCli: cmdAtomicSemCli :: MonadClient m => CmdAtomic -> m ()
+ Game.LambdaHack.Client.AtomicSemCli: drawCmdAtomicUI :: MonadClientUI m => Bool -> CmdAtomic -> m ()
+ Game.LambdaHack.Client.AtomicSemCli: drawSfxAtomicUI :: MonadClientUI m => Bool -> SfxAtomic -> m ()
+ Game.LambdaHack.Client.Binding: Binding :: Map KM (Text, Bool, HumanCmd) -> Map KM KM -> [KM] -> [KM] -> Map HumanCmd KM -> Binding
+ Game.LambdaHack.Client.Binding: data Binding
+ Game.LambdaHack.Client.Binding: kcmd :: Binding -> Map KM (Text, Bool, HumanCmd)
+ Game.LambdaHack.Client.Binding: keyHelp :: Binding -> Slideshow
+ Game.LambdaHack.Client.Binding: kmacro :: Binding -> Map KM KM
+ Game.LambdaHack.Client.Binding: kmajor :: Binding -> [KM]
+ Game.LambdaHack.Client.Binding: kminor :: Binding -> [KM]
+ Game.LambdaHack.Client.Binding: krevMap :: Binding -> Map HumanCmd KM
+ Game.LambdaHack.Client.Binding: stdBinding :: ConfigUI -> Binding
+ Game.LambdaHack.Client.ClientSem: continueRun :: MonadClientAbort m => ActorId -> (Vector, Int) -> m CmdSer
+ Game.LambdaHack.Client.ClientSem: humanCommand :: (MonadClientAbort m, MonadClientUI m) => Msg -> m CmdSer
+ Game.LambdaHack.Client.ClientSem: queryAI :: MonadClient m => ActorId -> m CmdSer
+ Game.LambdaHack.Client.ClientSem: queryAIPick :: MonadClient m => ActorId -> m CmdSer
+ Game.LambdaHack.Client.ClientSem: queryUI :: (MonadClientAbort m, MonadClientUI m) => ActorId -> m CmdSer
+ Game.LambdaHack.Client.Config: ConfigUI :: ![(KM, HumanCmd)] -> !FilePath -> !FilePath -> ![(KM, KM)] -> !String -> !Int -> ConfigUI
+ Game.LambdaHack.Client.Config: configAppDataDirUI :: ConfigUI -> !FilePath
+ Game.LambdaHack.Client.Config: configCommands :: ConfigUI -> ![(KM, HumanCmd)]
+ Game.LambdaHack.Client.Config: configFont :: ConfigUI -> !String
+ Game.LambdaHack.Client.Config: configHistoryMax :: ConfigUI -> !Int
+ Game.LambdaHack.Client.Config: configMacros :: ConfigUI -> ![(KM, KM)]
+ Game.LambdaHack.Client.Config: configUICfgFile :: ConfigUI -> !FilePath
+ Game.LambdaHack.Client.Config: data ConfigUI
+ Game.LambdaHack.Client.Config: instance Binary ConfigUI
+ Game.LambdaHack.Client.Config: instance NFData ConfigUI
+ Game.LambdaHack.Client.Config: instance Show ConfigUI
+ Game.LambdaHack.Client.Draw: ColorBW :: ColorMode
+ Game.LambdaHack.Client.Draw: ColorFull :: ColorMode
+ Game.LambdaHack.Client.Draw: data ColorMode
+ Game.LambdaHack.Client.Draw: draw :: ColorMode -> COps -> Perception -> LevelId -> Maybe ActorId -> StateClient -> State -> Overlay -> SingleFrame
+ Game.LambdaHack.Client.HumanCmd: Accept :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Apply :: [Trigger] -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: ApplyItem :: Part -> Part -> Char -> Trigger
+ Game.LambdaHack.Client.HumanCmd: BumpFeature :: Part -> Part -> Feature -> Trigger
+ Game.LambdaHack.Client.HumanCmd: Cancel :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: CfgDump :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Clear :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Drop :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: EpsIncr :: Bool -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: GameExit :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: GameRestart :: Text -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: GameSave :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Help :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: History :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Inventory :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: MarkSmell :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: MarkSuspect :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: MarkVision :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: MemberBack :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: MemberCycle :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Move :: VectorXY -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Pickup :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Project :: [Trigger] -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Run :: VectorXY -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: SelectHero :: Int -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: TgtAscend :: Int -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: TgtEnemy :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: TgtFloor :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: TriggerDir :: [Trigger] -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: TriggerTile :: [Trigger] -> HumanCmd
+ Game.LambdaHack.Client.HumanCmd: Wait :: HumanCmd
+ Game.LambdaHack.Client.HumanCmd: cmdDescription :: HumanCmd -> Text
+ Game.LambdaHack.Client.HumanCmd: data HumanCmd
+ Game.LambdaHack.Client.HumanCmd: data Trigger
+ Game.LambdaHack.Client.HumanCmd: feature :: Trigger -> Feature
+ Game.LambdaHack.Client.HumanCmd: instance Binary HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Binary Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_0HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_0Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_10HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_11HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_12HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_13HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_14HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_15HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_16HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_17HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_18HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_19HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_1HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_1Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_20HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_21HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_22HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_23HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_24HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_25HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_26HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_27HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_28HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_2HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_3HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_4HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_5HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_6HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_7HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_8HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Constructor C1_9HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Datatype D1HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Datatype D1Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Eq HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Eq Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Generic HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Generic Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Ord HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Ord Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Read HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Read Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Selector S1_0_0Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Selector S1_0_1Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Selector S1_0_2Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Selector S1_1_0Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Selector S1_1_1Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Selector S1_1_2Trigger
+ Game.LambdaHack.Client.HumanCmd: instance Show HumanCmd
+ Game.LambdaHack.Client.HumanCmd: instance Show Trigger
+ Game.LambdaHack.Client.HumanCmd: majorHumanCmd :: HumanCmd -> Bool
+ Game.LambdaHack.Client.HumanCmd: minorHumanCmd :: HumanCmd -> Bool
+ Game.LambdaHack.Client.HumanCmd: noRemoteHumanCmd :: HumanCmd -> Bool
+ Game.LambdaHack.Client.HumanCmd: object :: Trigger -> Part
+ Game.LambdaHack.Client.HumanCmd: symbol :: Trigger -> Char
+ Game.LambdaHack.Client.HumanCmd: verb :: Trigger -> Part
+ Game.LambdaHack.Client.HumanGlobal: applyHuman :: (MonadClientAbort m, MonadClientUI m) => [Trigger] -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: cfgDumpHuman :: MonadClientUI m => m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: dropHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: exploreLeader :: MonadClientUI m => Vector -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: gameExitHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: gameRestartHuman :: (MonadClientAbort m, MonadClientUI m) => Text -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: gameSaveHuman :: MonadClientUI m => m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: instance Eq ItemDialogState
+ Game.LambdaHack.Client.HumanGlobal: moveLeader :: MonadClientUI m => Vector -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: pickupHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: projectLeader :: (MonadClientAbort m, MonadClientUI m) => [Trigger] -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: runLeader :: MonadClientUI m => Vector -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: triggerDirHuman :: (MonadClientAbort m, MonadClientUI m) => [Trigger] -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: triggerTileHuman :: (MonadClientAbort m, MonadClientUI m) => [Trigger] -> m CmdSer
+ Game.LambdaHack.Client.HumanGlobal: waitHuman :: MonadClientUI m => m CmdSer
+ Game.LambdaHack.Client.HumanLocal: acceptHuman :: MonadClientUI m => WriterT Slideshow m () -> WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: cancelHuman :: MonadClientUI m => WriterT Slideshow m () -> WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: clearHuman :: Monad m => m ()
+ Game.LambdaHack.Client.HumanLocal: displayMainMenu :: MonadClientUI m => WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: endTargeting :: MonadClientUI m => Bool -> m ()
+ Game.LambdaHack.Client.HumanLocal: epsIncrHuman :: MonadClientAbort m => Bool -> m ()
+ Game.LambdaHack.Client.HumanLocal: floorItemOverlay :: MonadClient m => ItemBag -> m Overlay
+ Game.LambdaHack.Client.HumanLocal: helpHuman :: MonadClientUI m => WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: historyHuman :: MonadClientUI m => WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: humanMarkSmell :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.HumanLocal: humanMarkSuspect :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.HumanLocal: humanMarkVision :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.HumanLocal: inventoryHuman :: (MonadClientAbort m, MonadClientUI m) => WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: itemOverlay :: MonadClient m => ItemBag -> ItemInv -> m Overlay
+ Game.LambdaHack.Client.HumanLocal: lookAt :: MonadClientUI m => Bool -> Bool -> Point -> ActorId -> Text -> m Text
+ Game.LambdaHack.Client.HumanLocal: memberBackHuman :: (MonadClientAbort m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.HumanLocal: memberCycleHuman :: (MonadClientAbort m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.HumanLocal: moveCursor :: MonadClientUI m => Vector -> Int -> WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: retargetLeader :: MonadClientUI m => WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: selectHeroHuman :: (MonadClientAbort m, MonadClientUI m) => Int -> m ()
+ Game.LambdaHack.Client.HumanLocal: selectLeader :: MonadClientUI m => ActorId -> m Bool
+ Game.LambdaHack.Client.HumanLocal: stopRunning :: MonadClient m => m ()
+ Game.LambdaHack.Client.HumanLocal: tgtAscendHuman :: (MonadClientAbort m, MonadClientUI m) => Int -> WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: tgtEnemyLeader :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: tgtFloorLeader :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()
+ Game.LambdaHack.Client.HumanLocal: viewedLevel :: MonadClientUI m => m (LevelId, Level)
+ Game.LambdaHack.Client.HumanSem: cmdHumanSem :: (MonadClientAbort m, MonadClientUI m) => HumanCmd -> WriterT Slideshow m (Maybe CmdSer)
+ Game.LambdaHack.Client.LoopAction: loopAI :: MonadConnClient CmdClientAI m => (CmdClientAI -> m ()) -> m ()
+ Game.LambdaHack.Client.LoopAction: loopUI :: (MonadClientUI m, MonadConnClient CmdClientUI m) => (CmdClientUI -> m ()) -> m ()
+ Game.LambdaHack.Client.RunAction: canRun :: MonadClient m => ActorId -> (Vector, Int) -> m Bool
+ Game.LambdaHack.Client.RunAction: continueRunDir :: MonadClientAbort m => ActorId -> (Vector, Int) -> m (Vector, Int)
+ Game.LambdaHack.Client.State: StateClient :: !(Maybe TgtMode) -> !(Maybe Point) -> !Int -> !(EnumMap ActorId Target) -> !(Maybe (Vector, Int)) -> !Report -> !History -> ![Atomic] -> !Discovery -> !FactionPers -> !StdGen -> !ConfigUI -> !(Maybe KM) -> !(Maybe ActorId) -> !FactionId -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> StateClient
+ Game.LambdaHack.Client.State: TEnemy :: ActorId -> Point -> Target
+ Game.LambdaHack.Client.State: TPos :: Point -> Target
+ Game.LambdaHack.Client.State: TgtAuto :: !LevelId -> TgtMode
+ Game.LambdaHack.Client.State: TgtExplicit :: !LevelId -> TgtMode
+ Game.LambdaHack.Client.State: _sleader :: StateClient -> !(Maybe ActorId)
+ Game.LambdaHack.Client.State: _sside :: StateClient -> !FactionId
+ Game.LambdaHack.Client.State: data StateClient
+ Game.LambdaHack.Client.State: data Target
+ Game.LambdaHack.Client.State: data TgtMode
+ Game.LambdaHack.Client.State: defHistory :: IO History
+ Game.LambdaHack.Client.State: defStateClient :: History -> ConfigUI -> FactionId -> Bool -> StateClient
+ Game.LambdaHack.Client.State: getTarget :: ActorId -> StateClient -> Maybe Target
+ Game.LambdaHack.Client.State: instance Binary StateClient
+ Game.LambdaHack.Client.State: instance Binary Target
+ Game.LambdaHack.Client.State: instance Binary TgtMode
+ Game.LambdaHack.Client.State: instance Eq Target
+ Game.LambdaHack.Client.State: instance Eq TgtMode
+ Game.LambdaHack.Client.State: instance Show StateClient
+ Game.LambdaHack.Client.State: instance Show Target
+ Game.LambdaHack.Client.State: instance Show TgtMode
+ Game.LambdaHack.Client.State: sconfigUI :: StateClient -> !ConfigUI
+ Game.LambdaHack.Client.State: scursor :: StateClient -> !(Maybe Point)
+ Game.LambdaHack.Client.State: sdebugCli :: StateClient -> !Bool
+ Game.LambdaHack.Client.State: sdisco :: StateClient -> !Discovery
+ Game.LambdaHack.Client.State: seps :: StateClient -> !Int
+ Game.LambdaHack.Client.State: sfper :: StateClient -> !FactionPers
+ Game.LambdaHack.Client.State: shistory :: StateClient -> !History
+ Game.LambdaHack.Client.State: sisAI :: StateClient -> !Bool
+ Game.LambdaHack.Client.State: slastKey :: StateClient -> !(Maybe KM)
+ Game.LambdaHack.Client.State: smarkSmell :: StateClient -> !Bool
+ Game.LambdaHack.Client.State: smarkSuspect :: StateClient -> !Bool
+ Game.LambdaHack.Client.State: smarkVision :: StateClient -> !Bool
+ Game.LambdaHack.Client.State: squit :: StateClient -> !Bool
+ Game.LambdaHack.Client.State: srandom :: StateClient -> !StdGen
+ Game.LambdaHack.Client.State: sreport :: StateClient -> !Report
+ Game.LambdaHack.Client.State: srunning :: StateClient -> !(Maybe (Vector, Int))
+ Game.LambdaHack.Client.State: sside :: StateClient -> FactionId
+ Game.LambdaHack.Client.State: stargetD :: StateClient -> !(EnumMap ActorId Target)
+ Game.LambdaHack.Client.State: stgtMode :: StateClient -> !(Maybe TgtMode)
+ Game.LambdaHack.Client.State: sundo :: StateClient -> ![Atomic]
+ Game.LambdaHack.Client.State: tgtLevelId :: TgtMode -> !LevelId
+ Game.LambdaHack.Client.State: toggleMarkSmell :: StateClient -> StateClient
+ Game.LambdaHack.Client.State: toggleMarkSuspect :: StateClient -> StateClient
+ Game.LambdaHack.Client.State: toggleMarkVision :: StateClient -> StateClient
+ Game.LambdaHack.Client.State: updateLeader :: ActorId -> State -> StateClient -> StateClient
+ Game.LambdaHack.Client.State: updateTarget :: ActorId -> (Maybe Target -> Maybe Target) -> StateClient -> StateClient
+ Game.LambdaHack.Client.Strategy: (.=>) :: Bool -> Strategy a -> Strategy a
+ Game.LambdaHack.Client.Strategy: (.|) :: Strategy a -> Strategy a -> Strategy a
+ Game.LambdaHack.Client.Strategy: bestVariant :: Strategy a -> Frequency a
+ Game.LambdaHack.Client.Strategy: data Strategy a
+ Game.LambdaHack.Client.Strategy: instance Monad Strategy
+ Game.LambdaHack.Client.Strategy: instance MonadPlus Strategy
+ Game.LambdaHack.Client.Strategy: instance Show a => Show (Strategy a)
+ Game.LambdaHack.Client.Strategy: liftFrequency :: Frequency a -> Strategy a
+ Game.LambdaHack.Client.Strategy: nullStrategy :: Strategy a -> Bool
+ Game.LambdaHack.Client.Strategy: only :: (a -> Bool) -> Strategy a -> Strategy a
+ Game.LambdaHack.Client.Strategy: reject :: Strategy a
+ Game.LambdaHack.Client.Strategy: renameStrategy :: Text -> Strategy a -> Strategy a
+ Game.LambdaHack.Client.Strategy: returN :: Text -> a -> Strategy a
+ Game.LambdaHack.Client.StrategyAction: actionStrategy :: MonadClient m => ActorId -> [Ability] -> m (Strategy CmdSer)
+ Game.LambdaHack.Client.StrategyAction: targetStrategy :: MonadClient m => ActorId -> [Ability] -> m (Strategy (Maybe Target))
+ Game.LambdaHack.Client.StrategyAction: visibleFoes :: MonadActionRO m => FactionPers -> ActorId -> m [(ActorId, Actor)]
+ Game.LambdaHack.Common.Ability: Chase :: Ability
+ Game.LambdaHack.Common.Ability: Flee :: Ability
+ Game.LambdaHack.Common.Ability: Heal :: Ability
+ Game.LambdaHack.Common.Ability: Melee :: Ability
+ Game.LambdaHack.Common.Ability: Pickup :: Ability
+ Game.LambdaHack.Common.Ability: Ranged :: Ability
+ Game.LambdaHack.Common.Ability: Tools :: Ability
+ Game.LambdaHack.Common.Ability: Track :: Ability
+ Game.LambdaHack.Common.Ability: Wander :: Ability
+ Game.LambdaHack.Common.Ability: data Ability
+ Game.LambdaHack.Common.Ability: instance Bounded Ability
+ Game.LambdaHack.Common.Ability: instance Enum Ability
+ Game.LambdaHack.Common.Ability: instance Eq Ability
+ Game.LambdaHack.Common.Ability: instance Ord Ability
+ Game.LambdaHack.Common.Ability: instance Show Ability
+ Game.LambdaHack.Common.Action: class MonadActionRO m => MonadAction m
+ Game.LambdaHack.Common.Action: class (Monad m, Functor m) => MonadActionRO m
+ Game.LambdaHack.Common.Action: class MonadActionRO m => MonadAtomic m where execCmdAtomic = execAtomic . CmdAtomic execSfxAtomic = execAtomic . SfxAtomic
+ Game.LambdaHack.Common.Action: execAtomic :: MonadAtomic m => Atomic -> m ()
+ Game.LambdaHack.Common.Action: execCmdAtomic :: MonadAtomic m => CmdAtomic -> m ()
+ Game.LambdaHack.Common.Action: execSfxAtomic :: MonadAtomic m => SfxAtomic -> m ()
+ Game.LambdaHack.Common.Action: getState :: MonadActionRO m => m State
+ Game.LambdaHack.Common.Action: getsLevel :: MonadActionRO m => LevelId -> (Level -> a) -> m a
+ Game.LambdaHack.Common.Action: getsState :: MonadActionRO m => (State -> a) -> m a
+ Game.LambdaHack.Common.Action: instance (Monoid a, MonadActionRO m) => MonadActionRO (WriterT a m)
+ Game.LambdaHack.Common.Action: instance MonadActionRO m => Show (WriterT a m b)
+ Game.LambdaHack.Common.Action: modifyState :: MonadAction m => (State -> State) -> m ()
+ Game.LambdaHack.Common.Action: nHumans :: MonadActionRO m => m Int
+ Game.LambdaHack.Common.Action: putState :: MonadAction m => State -> m ()
+ Game.LambdaHack.Common.Actor: Actor :: !(Id ActorKind) -> !(Maybe Char) -> !(Maybe Text) -> !(Maybe Color) -> !(Maybe Speed) -> !Int -> !(Maybe [Vector]) -> !Point -> !Point -> !LevelId -> !ItemBag -> !ItemInv -> !InvChar -> !Time -> !Time -> !FactionId -> !Bool -> Actor
+ Game.LambdaHack.Common.Actor: InvChar :: Char -> InvChar
+ Game.LambdaHack.Common.Actor: _bname :: Actor -> !(Maybe Text)
+ Game.LambdaHack.Common.Actor: actorSpeed :: Ops ActorKind -> Actor -> Speed
+ Game.LambdaHack.Common.Actor: actorTemplate :: Id ActorKind -> Maybe Char -> Maybe Text -> Maybe Color -> Maybe Speed -> Int -> Maybe [Vector] -> Point -> LevelId -> Time -> FactionId -> Bool -> Actor
+ Game.LambdaHack.Common.Actor: allLetters :: [InvChar]
+ Game.LambdaHack.Common.Actor: assignLetter :: ItemId -> Maybe InvChar -> Actor -> Maybe InvChar
+ Game.LambdaHack.Common.Actor: bbag :: Actor -> !ItemBag
+ Game.LambdaHack.Common.Actor: bcolor :: Actor -> !(Maybe Color)
+ Game.LambdaHack.Common.Actor: bfid :: Actor -> !FactionId
+ Game.LambdaHack.Common.Actor: bhp :: Actor -> !Int
+ Game.LambdaHack.Common.Actor: binv :: Actor -> !ItemInv
+ Game.LambdaHack.Common.Actor: bkind :: Actor -> !(Id ActorKind)
+ Game.LambdaHack.Common.Actor: bletter :: Actor -> !InvChar
+ Game.LambdaHack.Common.Actor: blid :: Actor -> !LevelId
+ Game.LambdaHack.Common.Actor: boldpos :: Actor -> !Point
+ Game.LambdaHack.Common.Actor: bpath :: Actor -> !(Maybe [Vector])
+ Game.LambdaHack.Common.Actor: bpos :: Actor -> !Point
+ Game.LambdaHack.Common.Actor: bproj :: Actor -> !Bool
+ Game.LambdaHack.Common.Actor: braced :: Actor -> Time -> Bool
+ Game.LambdaHack.Common.Actor: bspeed :: Actor -> !(Maybe Speed)
+ Game.LambdaHack.Common.Actor: bsymbol :: Actor -> !(Maybe Char)
+ Game.LambdaHack.Common.Actor: btime :: Actor -> !Time
+ Game.LambdaHack.Common.Actor: bwait :: Actor -> !Time
+ Game.LambdaHack.Common.Actor: data Actor
+ Game.LambdaHack.Common.Actor: data ActorId
+ Game.LambdaHack.Common.Actor: heroKindId :: Ops ActorKind -> Id ActorKind
+ Game.LambdaHack.Common.Actor: instance (Binary k, Binary v, Eq k, Hashable k) => Binary (HashMap k v)
+ Game.LambdaHack.Common.Actor: instance Binary Actor
+ Game.LambdaHack.Common.Actor: instance Binary ActorId
+ Game.LambdaHack.Common.Actor: instance Binary InvChar
+ Game.LambdaHack.Common.Actor: instance Enum ActorId
+ Game.LambdaHack.Common.Actor: instance Enum InvChar
+ Game.LambdaHack.Common.Actor: instance Eq Actor
+ Game.LambdaHack.Common.Actor: instance Eq ActorId
+ Game.LambdaHack.Common.Actor: instance Eq InvChar
+ Game.LambdaHack.Common.Actor: instance Ord Actor
+ Game.LambdaHack.Common.Actor: instance Ord ActorId
+ Game.LambdaHack.Common.Actor: instance Ord InvChar
+ Game.LambdaHack.Common.Actor: instance Show Actor
+ Game.LambdaHack.Common.Actor: instance Show ActorId
+ Game.LambdaHack.Common.Actor: instance Show InvChar
+ Game.LambdaHack.Common.Actor: invChar :: InvChar -> Char
+ Game.LambdaHack.Common.Actor: letterLabel :: InvChar -> Part
+ Game.LambdaHack.Common.Actor: letterRange :: [InvChar] -> Text
+ Game.LambdaHack.Common.Actor: mapActorItems_ :: Monad m => (ItemId -> Int -> m a) -> Actor -> m ()
+ Game.LambdaHack.Common.Actor: monsterGenChance :: Int -> Int -> Int -> Rnd Bool
+ Game.LambdaHack.Common.Actor: newtype InvChar
+ Game.LambdaHack.Common.Actor: partActor :: Ops ActorKind -> Actor -> Part
+ Game.LambdaHack.Common.Actor: projectileKindId :: Ops ActorKind -> Id ActorKind
+ Game.LambdaHack.Common.Actor: rmFromBag :: Int -> ItemId -> ItemBag -> ItemBag
+ Game.LambdaHack.Common.Actor: smellTimeout :: Time
+ Game.LambdaHack.Common.Actor: timeAddFromSpeed :: Ops ActorKind -> Actor -> Time -> Time
+ Game.LambdaHack.Common.Actor: type ActorDict = EnumMap ActorId Actor
+ Game.LambdaHack.Common.Actor: type ItemBag = EnumMap ItemId Int
+ Game.LambdaHack.Common.Actor: type ItemDict = EnumMap ItemId Item
+ Game.LambdaHack.Common.Actor: type ItemInv = EnumMap InvChar ItemId
+ Game.LambdaHack.Common.Actor: type ItemRev = HashMap Item ItemId
+ Game.LambdaHack.Common.Actor: unoccupied :: [Actor] -> Point -> Bool
+ Game.LambdaHack.Common.ActorState: actorAssocs :: (FactionId -> Bool) -> LevelId -> State -> [(ActorId, Actor)]
+ Game.LambdaHack.Common.ActorState: actorContainer :: ActorId -> ItemInv -> ItemId -> Container
+ Game.LambdaHack.Common.ActorState: actorList :: (FactionId -> Bool) -> LevelId -> State -> [Actor]
+ Game.LambdaHack.Common.ActorState: actorNotProjAssocs :: (FactionId -> Bool) -> LevelId -> State -> [(ActorId, Actor)]
+ Game.LambdaHack.Common.ActorState: actorNotProjList :: (FactionId -> Bool) -> LevelId -> State -> [Actor]
+ Game.LambdaHack.Common.ActorState: calculateTotal :: Actor -> State -> (ItemBag, Int)
+ Game.LambdaHack.Common.ActorState: foesAdjacent :: X -> Y -> Point -> [Actor] -> Bool
+ Game.LambdaHack.Common.ActorState: getActorBag :: ActorId -> State -> ItemBag
+ Game.LambdaHack.Common.ActorState: getActorBody :: ActorId -> State -> Actor
+ Game.LambdaHack.Common.ActorState: getActorInv :: ActorId -> State -> ItemInv
+ Game.LambdaHack.Common.ActorState: getActorItem :: ActorId -> State -> [(ItemId, Item)]
+ Game.LambdaHack.Common.ActorState: getItemBody :: ItemId -> State -> Item
+ Game.LambdaHack.Common.ActorState: memActor :: ActorId -> LevelId -> State -> Bool
+ Game.LambdaHack.Common.ActorState: nearbyFreePoints :: Ops TileKind -> (Id TileKind -> Bool) -> Point -> LevelId -> State -> [Point]
+ Game.LambdaHack.Common.ActorState: posToActor :: Point -> LevelId -> State -> Maybe ActorId
+ Game.LambdaHack.Common.ActorState: tryFindHeroK :: State -> FactionId -> Int -> Maybe (ActorId, Actor)
+ Game.LambdaHack.Common.ActorState: updateActorBody :: ActorId -> (Actor -> Actor) -> State -> State
+ Game.LambdaHack.Common.ActorState: whereTo :: State -> LevelId -> Int -> Maybe (LevelId, Point)
+ Game.LambdaHack.Common.Animation: AcConfirm :: SingleFrame -> AcFrame
+ Game.LambdaHack.Common.Animation: AcDelay :: AcFrame
+ Game.LambdaHack.Common.Animation: AcNormal :: SingleFrame -> AcFrame
+ Game.LambdaHack.Common.Animation: AcRunning :: SingleFrame -> AcFrame
+ Game.LambdaHack.Common.Animation: Attr :: !Color -> !Color -> Attr
+ Game.LambdaHack.Common.Animation: AttrChar :: !Attr -> !Char -> AttrChar
+ Game.LambdaHack.Common.Animation: SingleFrame :: [[AttrChar]] -> Text -> Text -> SingleFrame
+ Game.LambdaHack.Common.Animation: acAttr :: AttrChar -> !Attr
+ Game.LambdaHack.Common.Animation: acChar :: AttrChar -> !Char
+ Game.LambdaHack.Common.Animation: bg :: Attr -> !Color
+ Game.LambdaHack.Common.Animation: blockHit :: (Point, Point) -> Color -> Color -> Animation
+ Game.LambdaHack.Common.Animation: blockMiss :: (Point, Point) -> Animation
+ Game.LambdaHack.Common.Animation: data AcFrame
+ Game.LambdaHack.Common.Animation: data Animation
+ Game.LambdaHack.Common.Animation: data Attr
+ Game.LambdaHack.Common.Animation: data AttrChar
+ Game.LambdaHack.Common.Animation: data SingleFrame
+ Game.LambdaHack.Common.Animation: deathBody :: Point -> Animation
+ Game.LambdaHack.Common.Animation: defAttr :: Attr
+ Game.LambdaHack.Common.Animation: emptySingleFrame :: SingleFrame
+ Game.LambdaHack.Common.Animation: fadeout :: Bool -> Bool -> X -> Y -> Rnd Animation
+ Game.LambdaHack.Common.Animation: fg :: Attr -> !Color
+ Game.LambdaHack.Common.Animation: instance Binary AcFrame
+ Game.LambdaHack.Common.Animation: instance Binary SingleFrame
+ Game.LambdaHack.Common.Animation: instance Eq AcFrame
+ Game.LambdaHack.Common.Animation: instance Eq Animation
+ Game.LambdaHack.Common.Animation: instance Eq SingleFrame
+ Game.LambdaHack.Common.Animation: instance Monoid Animation
+ Game.LambdaHack.Common.Animation: instance Show AcFrame
+ Game.LambdaHack.Common.Animation: instance Show Animation
+ Game.LambdaHack.Common.Animation: instance Show SingleFrame
+ Game.LambdaHack.Common.Animation: renderAnim :: X -> Y -> SingleFrame -> Animation -> Frames
+ Game.LambdaHack.Common.Animation: restrictAnim :: EnumSet Point -> Animation -> Animation
+ Game.LambdaHack.Common.Animation: sfBottom :: SingleFrame -> Text
+ Game.LambdaHack.Common.Animation: sfLevel :: SingleFrame -> [[AttrChar]]
+ Game.LambdaHack.Common.Animation: sfTop :: SingleFrame -> Text
+ Game.LambdaHack.Common.Animation: swapPlaces :: (Point, Point) -> Animation
+ Game.LambdaHack.Common.Animation: twirlSplash :: (Point, Point) -> Color -> Color -> Animation
+ Game.LambdaHack.Common.Animation: type Frames = [Maybe SingleFrame]
+ Game.LambdaHack.Common.Animation: xsizeSingleFrame :: SingleFrame -> X
+ Game.LambdaHack.Common.Animation: ysizeSingleFrame :: SingleFrame -> X
+ Game.LambdaHack.Common.Area: expand :: Area -> Int -> Area
+ Game.LambdaHack.Common.Area: grid :: (X, Y) -> Area -> [(PointXY, Area)]
+ Game.LambdaHack.Common.Area: insideXY :: PointXY -> Area -> Bool
+ Game.LambdaHack.Common.Area: normalizeArea :: Area -> Area
+ Game.LambdaHack.Common.Area: trivialArea :: Area -> Bool
+ Game.LambdaHack.Common.Area: type Area = (X, Y, X, Y)
+ Game.LambdaHack.Common.Area: validArea :: Area -> Bool
+ Game.LambdaHack.Common.Area: vicinityCardinalXY :: Area -> PointXY -> [PointXY]
+ Game.LambdaHack.Common.Area: vicinityXY :: Area -> PointXY -> [PointXY]
+ Game.LambdaHack.Common.AtomicCmd: ActivateD :: ActorId -> ItemId -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: AgeActorA :: ActorId -> Time -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: AgeGameA :: Time -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: AgeLevelA :: LevelId -> Time -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: AlterSmellA :: LevelId -> Point -> (Maybe Time) -> (Maybe Time) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: AlterTileA :: LevelId -> Point -> (Id TileKind) -> (Id TileKind) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: CatchD :: ActorId -> ItemId -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: CheckD :: ActorId -> ItemId -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: CmdAtomic :: CmdAtomic -> Atomic
+ Game.LambdaHack.Common.AtomicCmd: ColorActorA :: ActorId -> (Maybe Color) -> (Maybe Color) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: CoverA :: LevelId -> Point -> ItemId -> (Id ItemKind) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: CreateActorA :: ActorId -> Actor -> [(ItemId, Item)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: CreateItemA :: ItemId -> Item -> Int -> Container -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: DestroyActorA :: ActorId -> Actor -> [(ItemId, Item)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: DestroyItemA :: ItemId -> Item -> Int -> Container -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: DiplFactionA :: FactionId -> FactionId -> Diplomacy -> Diplomacy -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: DiscoverA :: LevelId -> Point -> ItemId -> (Id ItemKind) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: DisplaceActorA :: ActorId -> ActorId -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: DisplayDelayD :: FactionId -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: DisplayPushD :: FactionId -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: EffectD :: ActorId -> (Effect Int) -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: HasteActorA :: ActorId -> Speed -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: HealActorA :: ActorId -> Int -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: HitBlockD :: HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: HitD :: HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: KillExitA :: FactionId -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: LeadFactionA :: FactionId -> (Maybe ActorId) -> (Maybe ActorId) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: LoseActorA :: ActorId -> Actor -> [(ItemId, Item)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: LoseItemA :: ItemId -> Item -> Int -> Container -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: LoseSmellA :: LevelId -> [(Point, Time)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: LoseTileA :: LevelId -> [(Point, Id TileKind)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: MissBlockD :: HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: MoveActorA :: ActorId -> Point -> Point -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: MoveItemA :: ItemId -> Int -> Container -> Container -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: MsgAllA :: Msg -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: MsgAllD :: Msg -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: MsgFidD :: FactionId -> Msg -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: PathActorA :: ActorId -> (Maybe [Vector]) -> (Maybe [Vector]) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: PerceptionA :: LevelId -> PerActor -> PerActor -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: ProjectD :: ActorId -> ItemId -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: QuitFactionA :: FactionId -> (Maybe Actor) -> (Maybe Status) -> (Maybe Status) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: RecoilD :: ActorId -> ActorId -> Item -> HitAtomic -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: RestartA :: FactionId -> Discovery -> FactionPers -> State -> Bool -> Text -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: RestartServerA :: State -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: ResumeA :: FactionId -> FactionPers -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: ResumeServerA :: State -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: SaveBkpA :: CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: SaveExitA :: CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: SearchTileA :: ActorId -> Point -> (Id TileKind) -> (Id TileKind) -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: SfxAtomic :: SfxAtomic -> Atomic
+ Game.LambdaHack.Common.AtomicCmd: ShunD :: ActorId -> Point -> Feature -> Bool -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: SpotActorA :: ActorId -> Actor -> [(ItemId, Item)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: SpotItemA :: ItemId -> Item -> Int -> Container -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: SpotSmellA :: LevelId -> [(Point, Time)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: SpotTileA :: LevelId -> [(Point, Id TileKind)] -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: StrikeD :: ActorId -> ActorId -> Item -> HitAtomic -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: TriggerD :: ActorId -> Point -> Feature -> Bool -> SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: WaitActorA :: ActorId -> Time -> Time -> CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: data Atomic
+ Game.LambdaHack.Common.AtomicCmd: data CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: data HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: data SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Binary Atomic
+ Game.LambdaHack.Common.AtomicCmd: instance Binary CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Binary HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Binary SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_0Atomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_0CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_0HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_0SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_10CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_10SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_11CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_11SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_12CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_12SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_13CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_14CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_15CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_16CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_17CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_18CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_19CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_1Atomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_1CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_1HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_1SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_20CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_21CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_22CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_23CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_24CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_25CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_26CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_27CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_28CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_29CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_2CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_2HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_2SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_30CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_31CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_32CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_33CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_34CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_35CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_36CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_37CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_38CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_39CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_3CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_3SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_4CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_4SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_5CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_5SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_6CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_6SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_7CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_7SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_8CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_8SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_9CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Constructor C1_9SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Datatype D1Atomic
+ Game.LambdaHack.Common.AtomicCmd: instance Datatype D1CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Datatype D1HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Datatype D1SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Eq Atomic
+ Game.LambdaHack.Common.AtomicCmd: instance Eq CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Eq HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Eq SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Generic Atomic
+ Game.LambdaHack.Common.AtomicCmd: instance Generic CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Generic HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Generic SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Show Atomic
+ Game.LambdaHack.Common.AtomicCmd: instance Show CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Show HitAtomic
+ Game.LambdaHack.Common.AtomicCmd: instance Show SfxAtomic
+ Game.LambdaHack.Common.AtomicCmd: undoAtomic :: Atomic -> Maybe Atomic
+ Game.LambdaHack.Common.AtomicCmd: undoCmdAtomic :: CmdAtomic -> Maybe CmdAtomic
+ Game.LambdaHack.Common.AtomicCmd: undoSfxAtomic :: SfxAtomic -> SfxAtomic
+ Game.LambdaHack.Common.AtomicPos: PosAll :: PosAtomic
+ Game.LambdaHack.Common.AtomicPos: PosFid :: FactionId -> PosAtomic
+ Game.LambdaHack.Common.AtomicPos: PosFidAndSer :: FactionId -> PosAtomic
+ Game.LambdaHack.Common.AtomicPos: PosFidAndSight :: FactionId -> LevelId -> [Point] -> PosAtomic
+ Game.LambdaHack.Common.AtomicPos: PosNone :: PosAtomic
+ Game.LambdaHack.Common.AtomicPos: PosSer :: PosAtomic
+ Game.LambdaHack.Common.AtomicPos: PosSight :: LevelId -> [Point] -> PosAtomic
+ Game.LambdaHack.Common.AtomicPos: PosSmell :: LevelId -> [Point] -> PosAtomic
+ Game.LambdaHack.Common.AtomicPos: breakCmdAtomic :: MonadActionRO m => CmdAtomic -> m [CmdAtomic]
+ Game.LambdaHack.Common.AtomicPos: data PosAtomic
+ Game.LambdaHack.Common.AtomicPos: instance Eq PosAtomic
+ Game.LambdaHack.Common.AtomicPos: instance Show PosAtomic
+ Game.LambdaHack.Common.AtomicPos: loudCmdAtomic :: FactionId -> CmdAtomic -> Bool
+ Game.LambdaHack.Common.AtomicPos: posCmdAtomic :: MonadActionRO m => CmdAtomic -> m PosAtomic
+ Game.LambdaHack.Common.AtomicPos: posSfxAtomic :: MonadActionRO m => SfxAtomic -> m PosAtomic
+ Game.LambdaHack.Common.AtomicPos: resetsFovAtomic :: MonadActionRO m => CmdAtomic -> m (Maybe [FactionId])
+ Game.LambdaHack.Common.AtomicPos: seenAtomicCli :: Bool -> FactionId -> Perception -> PosAtomic -> Bool
+ Game.LambdaHack.Common.AtomicPos: seenAtomicSer :: PosAtomic -> Bool
+ Game.LambdaHack.Common.AtomicSem: cmdAtomicSem :: MonadAction m => CmdAtomic -> m ()
+ Game.LambdaHack.Common.AtomicSem: posOfAid :: MonadActionRO m => ActorId -> m (LevelId, Point)
+ Game.LambdaHack.Common.AtomicSem: posOfContainer :: MonadActionRO m => Container -> m (LevelId, Point)
+ Game.LambdaHack.Common.ClientCmd: ChanServer :: TQueue c -> TQueue CmdSer -> ChanServer c
+ Game.LambdaHack.Common.ClientCmd: CmdAtomicAI :: CmdAtomic -> CmdClientAI
+ Game.LambdaHack.Common.ClientCmd: CmdAtomicUI :: CmdAtomic -> CmdClientUI
+ Game.LambdaHack.Common.ClientCmd: CmdQueryAI :: ActorId -> CmdClientAI
+ Game.LambdaHack.Common.ClientCmd: CmdQueryUI :: ActorId -> CmdClientUI
+ Game.LambdaHack.Common.ClientCmd: SfxAtomicUI :: SfxAtomic -> CmdClientUI
+ Game.LambdaHack.Common.ClientCmd: data ChanServer c
+ Game.LambdaHack.Common.ClientCmd: data CmdClientAI
+ Game.LambdaHack.Common.ClientCmd: data CmdClientUI
+ Game.LambdaHack.Common.ClientCmd: debugAid :: MonadActionRO m => ActorId -> Text -> m Text
+ Game.LambdaHack.Common.ClientCmd: debugCmdClientAI :: MonadActionRO m => CmdClientAI -> m Text
+ Game.LambdaHack.Common.ClientCmd: debugCmdClientUI :: MonadActionRO m => CmdClientUI -> m Text
+ Game.LambdaHack.Common.ClientCmd: fromServer :: ChanServer c -> TQueue c
+ Game.LambdaHack.Common.ClientCmd: instance Show (ChanServer c)
+ Game.LambdaHack.Common.ClientCmd: instance Show CmdClientAI
+ Game.LambdaHack.Common.ClientCmd: instance Show CmdClientUI
+ Game.LambdaHack.Common.ClientCmd: toServer :: ChanServer c -> TQueue CmdSer
+ Game.LambdaHack.Common.ClientCmd: type ConnServerDict = EnumMap FactionId ConnServerFaction
+ Game.LambdaHack.Common.ClientCmd: type ConnServerFaction = ((ChanFrontend, ChanServer CmdClientUI), ChanServer CmdClientAI)
+ Game.LambdaHack.Common.Color: Attr :: !Color -> !Color -> Attr
+ Game.LambdaHack.Common.Color: AttrChar :: !Attr -> !Char -> AttrChar
+ Game.LambdaHack.Common.Color: Black :: Color
+ Game.LambdaHack.Common.Color: Blue :: Color
+ Game.LambdaHack.Common.Color: BrBlack :: Color
+ Game.LambdaHack.Common.Color: BrBlue :: Color
+ Game.LambdaHack.Common.Color: BrCyan :: Color
+ Game.LambdaHack.Common.Color: BrGreen :: Color
+ Game.LambdaHack.Common.Color: BrMagenta :: Color
+ Game.LambdaHack.Common.Color: BrRed :: Color
+ Game.LambdaHack.Common.Color: BrWhite :: Color
+ Game.LambdaHack.Common.Color: BrYellow :: Color
+ Game.LambdaHack.Common.Color: Brown :: Color
+ Game.LambdaHack.Common.Color: Cyan :: Color
+ Game.LambdaHack.Common.Color: Green :: Color
+ Game.LambdaHack.Common.Color: Magenta :: Color
+ Game.LambdaHack.Common.Color: Red :: Color
+ Game.LambdaHack.Common.Color: White :: Color
+ Game.LambdaHack.Common.Color: acAttr :: AttrChar -> !Attr
+ Game.LambdaHack.Common.Color: acChar :: AttrChar -> !Char
+ Game.LambdaHack.Common.Color: bg :: Attr -> !Color
+ Game.LambdaHack.Common.Color: brightCol :: [Color]
+ Game.LambdaHack.Common.Color: colorToRGB :: Color -> String
+ Game.LambdaHack.Common.Color: darkCol :: [Color]
+ Game.LambdaHack.Common.Color: data Attr
+ Game.LambdaHack.Common.Color: data AttrChar
+ Game.LambdaHack.Common.Color: data Color
+ Game.LambdaHack.Common.Color: defAttr :: Attr
+ Game.LambdaHack.Common.Color: defBG :: Color
+ Game.LambdaHack.Common.Color: defFG :: Color
+ Game.LambdaHack.Common.Color: fg :: Attr -> !Color
+ Game.LambdaHack.Common.Color: instance Binary Attr
+ Game.LambdaHack.Common.Color: instance Binary AttrChar
+ Game.LambdaHack.Common.Color: instance Binary Color
+ Game.LambdaHack.Common.Color: instance Bounded Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_0Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_10Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_11Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_12Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_13Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_14Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_15Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_1Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_2Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_3Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_4Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_5Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_6Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_7Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_8Color
+ Game.LambdaHack.Common.Color: instance Constructor C1_9Color
+ Game.LambdaHack.Common.Color: instance Datatype D1Color
+ Game.LambdaHack.Common.Color: instance Enum Color
+ Game.LambdaHack.Common.Color: instance Eq Attr
+ Game.LambdaHack.Common.Color: instance Eq AttrChar
+ Game.LambdaHack.Common.Color: instance Eq Color
+ Game.LambdaHack.Common.Color: instance Generic Color
+ Game.LambdaHack.Common.Color: instance Hashable Color
+ Game.LambdaHack.Common.Color: instance Ord Attr
+ Game.LambdaHack.Common.Color: instance Ord Color
+ Game.LambdaHack.Common.Color: instance Show Attr
+ Game.LambdaHack.Common.Color: instance Show AttrChar
+ Game.LambdaHack.Common.Color: instance Show Color
+ Game.LambdaHack.Common.Color: isBright :: Color -> Bool
+ Game.LambdaHack.Common.Color: legalBG :: [Color]
+ Game.LambdaHack.Common.Color: stdCol :: [Color]
+ Game.LambdaHack.Common.ContentDef: ContentDef :: (a -> Char) -> (a -> Text) -> (a -> Freqs) -> ([a] -> [a]) -> [a] -> ContentDef a
+ Game.LambdaHack.Common.ContentDef: content :: ContentDef a -> [a]
+ Game.LambdaHack.Common.ContentDef: data ContentDef a
+ Game.LambdaHack.Common.ContentDef: getFreq :: ContentDef a -> a -> Freqs
+ Game.LambdaHack.Common.ContentDef: getName :: ContentDef a -> a -> Text
+ Game.LambdaHack.Common.ContentDef: getSymbol :: ContentDef a -> a -> Char
+ Game.LambdaHack.Common.ContentDef: validate :: ContentDef a -> [a] -> [a]
+ Game.LambdaHack.Common.Effect: ApplyPerfume :: Effect a
+ Game.LambdaHack.Common.Effect: Ascend :: Int -> Effect a
+ Game.LambdaHack.Common.Effect: CallFriend :: Int -> Effect a
+ Game.LambdaHack.Common.Effect: CreateItem :: Int -> Effect a
+ Game.LambdaHack.Common.Effect: Descend :: Int -> Effect a
+ Game.LambdaHack.Common.Effect: Dominate :: Effect a
+ Game.LambdaHack.Common.Effect: Escape :: Effect a
+ Game.LambdaHack.Common.Effect: Heal :: Int -> Effect a
+ Game.LambdaHack.Common.Effect: Hurt :: !RollDice -> a -> Effect a
+ Game.LambdaHack.Common.Effect: Mindprobe :: Int -> Effect a
+ Game.LambdaHack.Common.Effect: NoEffect :: Effect a
+ Game.LambdaHack.Common.Effect: Regeneration :: a -> Effect a
+ Game.LambdaHack.Common.Effect: Searching :: a -> Effect a
+ Game.LambdaHack.Common.Effect: Summon :: Int -> Effect a
+ Game.LambdaHack.Common.Effect: data Effect a
+ Game.LambdaHack.Common.Effect: effectToBenefit :: Effect Int -> Int
+ Game.LambdaHack.Common.Effect: effectToSuffix :: Effect Int -> Text
+ Game.LambdaHack.Common.Effect: effectTrav :: Effect a -> (a -> State s b) -> State s (Effect b)
+ Game.LambdaHack.Common.Effect: instance Binary a => Binary (Effect a)
+ Game.LambdaHack.Common.Effect: instance Constructor C1_0Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_10Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_11Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_12Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_13Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_1Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_2Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_3Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_4Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_5Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_6Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_7Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_8Effect
+ Game.LambdaHack.Common.Effect: instance Constructor C1_9Effect
+ Game.LambdaHack.Common.Effect: instance Datatype D1Effect
+ Game.LambdaHack.Common.Effect: instance Eq a => Eq (Effect a)
+ Game.LambdaHack.Common.Effect: instance Functor Effect
+ Game.LambdaHack.Common.Effect: instance Generic (Effect a)
+ Game.LambdaHack.Common.Effect: instance Hashable a => Hashable (Effect a)
+ Game.LambdaHack.Common.Effect: instance Ord a => Ord (Effect a)
+ Game.LambdaHack.Common.Effect: instance Read a => Read (Effect a)
+ Game.LambdaHack.Common.Effect: instance Show a => Show (Effect a)
+ Game.LambdaHack.Common.Faction: Alliance :: Diplomacy
+ Game.LambdaHack.Common.Faction: Camping :: Outcome
+ Game.LambdaHack.Common.Faction: Conquer :: Outcome
+ Game.LambdaHack.Common.Faction: Defeated :: Outcome
+ Game.LambdaHack.Common.Faction: Escape :: Outcome
+ Game.LambdaHack.Common.Faction: Faction :: !(Id FactionKind) -> !Text -> !Text -> !(Maybe (Id StrategyKind)) -> !(Maybe (Id StrategyKind)) -> !Dipl -> !(Maybe Status) -> !(Maybe ActorId) -> !Color -> !Int -> !LevelId -> Faction
+ Game.LambdaHack.Common.Faction: Killed :: Outcome
+ Game.LambdaHack.Common.Faction: Neutral :: Diplomacy
+ Game.LambdaHack.Common.Faction: Restart :: Outcome
+ Game.LambdaHack.Common.Faction: Status :: Outcome -> Int -> Text -> Status
+ Game.LambdaHack.Common.Faction: Unknown :: Diplomacy
+ Game.LambdaHack.Common.Faction: War :: Diplomacy
+ Game.LambdaHack.Common.Faction: data Diplomacy
+ Game.LambdaHack.Common.Faction: data Faction
+ Game.LambdaHack.Common.Faction: data FactionId
+ Game.LambdaHack.Common.Faction: data Outcome
+ Game.LambdaHack.Common.Faction: data Status
+ Game.LambdaHack.Common.Faction: gAiLeader :: Faction -> !(Maybe (Id StrategyKind))
+ Game.LambdaHack.Common.Faction: gAiMember :: Faction -> !(Maybe (Id StrategyKind))
+ Game.LambdaHack.Common.Faction: gcolor :: Faction -> !Color
+ Game.LambdaHack.Common.Faction: gconfig :: Faction -> !Text
+ Game.LambdaHack.Common.Faction: gdipl :: Faction -> !Dipl
+ Game.LambdaHack.Common.Faction: gentry :: Faction -> !LevelId
+ Game.LambdaHack.Common.Faction: ginitial :: Faction -> !Int
+ Game.LambdaHack.Common.Faction: gkind :: Faction -> !(Id FactionKind)
+ Game.LambdaHack.Common.Faction: gleader :: Faction -> !(Maybe ActorId)
+ Game.LambdaHack.Common.Faction: gname :: Faction -> !Text
+ Game.LambdaHack.Common.Faction: gquit :: Faction -> !(Maybe Status)
+ Game.LambdaHack.Common.Faction: instance Binary Diplomacy
+ Game.LambdaHack.Common.Faction: instance Binary Faction
+ Game.LambdaHack.Common.Faction: instance Binary Outcome
+ Game.LambdaHack.Common.Faction: instance Binary Status
+ Game.LambdaHack.Common.Faction: instance Eq Diplomacy
+ Game.LambdaHack.Common.Faction: instance Eq Faction
+ Game.LambdaHack.Common.Faction: instance Eq Outcome
+ Game.LambdaHack.Common.Faction: instance Eq Status
+ Game.LambdaHack.Common.Faction: instance Ord Diplomacy
+ Game.LambdaHack.Common.Faction: instance Ord Outcome
+ Game.LambdaHack.Common.Faction: instance Ord Status
+ Game.LambdaHack.Common.Faction: instance Show Diplomacy
+ Game.LambdaHack.Common.Faction: instance Show Faction
+ Game.LambdaHack.Common.Faction: instance Show Outcome
+ Game.LambdaHack.Common.Faction: instance Show Status
+ Game.LambdaHack.Common.Faction: isAllied :: Faction -> FactionId -> Bool
+ Game.LambdaHack.Common.Faction: isAtWar :: Faction -> FactionId -> Bool
+ Game.LambdaHack.Common.Faction: isHumanFact :: Faction -> Bool
+ Game.LambdaHack.Common.Faction: isSpawnFact :: COps -> Faction -> Bool
+ Game.LambdaHack.Common.Faction: isSummonFact :: COps -> Faction -> Bool
+ Game.LambdaHack.Common.Faction: stDepth :: Status -> Int
+ Game.LambdaHack.Common.Faction: stInfo :: Status -> Text
+ Game.LambdaHack.Common.Faction: stOutcome :: Status -> Outcome
+ Game.LambdaHack.Common.Faction: type FactionDict = EnumMap FactionId Faction
+ Game.LambdaHack.Common.Faction: usesAIFact :: Faction -> Bool
+ Game.LambdaHack.Common.Feature: Ascendable :: Feature
+ Game.LambdaHack.Common.Feature: Aura :: !(Effect Int) -> Feature
+ Game.LambdaHack.Common.Feature: CanActor :: Feature
+ Game.LambdaHack.Common.Feature: CanItem :: Feature
+ Game.LambdaHack.Common.Feature: Cause :: !(Effect Int) -> Feature
+ Game.LambdaHack.Common.Feature: ChangeTo :: !Text -> Feature
+ Game.LambdaHack.Common.Feature: Clear :: Feature
+ Game.LambdaHack.Common.Feature: Closable :: Feature
+ Game.LambdaHack.Common.Feature: Descendable :: Feature
+ Game.LambdaHack.Common.Feature: Escapable :: Feature
+ Game.LambdaHack.Common.Feature: Exit :: Feature
+ Game.LambdaHack.Common.Feature: HiddenAs :: !Text -> Feature
+ Game.LambdaHack.Common.Feature: Impenetrable :: Feature
+ Game.LambdaHack.Common.Feature: Lit :: Feature
+ Game.LambdaHack.Common.Feature: Openable :: Feature
+ Game.LambdaHack.Common.Feature: Path :: Feature
+ Game.LambdaHack.Common.Feature: Suspect :: Feature
+ Game.LambdaHack.Common.Feature: Walkable :: Feature
+ Game.LambdaHack.Common.Feature: data Feature
+ Game.LambdaHack.Common.Feature: instance Binary Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_0Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_10Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_11Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_12Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_13Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_14Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_15Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_16Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_17Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_1Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_2Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_3Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_4Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_5Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_6Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_7Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_8Feature
+ Game.LambdaHack.Common.Feature: instance Constructor C1_9Feature
+ Game.LambdaHack.Common.Feature: instance Datatype D1Feature
+ Game.LambdaHack.Common.Feature: instance Eq Feature
+ Game.LambdaHack.Common.Feature: instance Generic Feature
+ Game.LambdaHack.Common.Feature: instance Ord Feature
+ Game.LambdaHack.Common.Feature: instance Read Feature
+ Game.LambdaHack.Common.Feature: instance Show Feature
+ Game.LambdaHack.Common.Flavour: colorToFancyName :: Color -> Text
+ Game.LambdaHack.Common.Flavour: colorToPlainName :: Color -> Text
+ Game.LambdaHack.Common.Flavour: colorToTeamName :: Color -> Text
+ Game.LambdaHack.Common.Flavour: data Flavour
+ Game.LambdaHack.Common.Flavour: flavourToColor :: Flavour -> Color
+ Game.LambdaHack.Common.Flavour: flavourToName :: Flavour -> Text
+ Game.LambdaHack.Common.Flavour: instance Binary Flavour
+ Game.LambdaHack.Common.Flavour: instance Constructor C1_0Flavour
+ Game.LambdaHack.Common.Flavour: instance Datatype D1Flavour
+ Game.LambdaHack.Common.Flavour: instance Eq Flavour
+ Game.LambdaHack.Common.Flavour: instance Generic Flavour
+ Game.LambdaHack.Common.Flavour: instance Hashable Flavour
+ Game.LambdaHack.Common.Flavour: instance Ord Flavour
+ Game.LambdaHack.Common.Flavour: instance Selector S1_0_0Flavour
+ Game.LambdaHack.Common.Flavour: instance Selector S1_0_1Flavour
+ Game.LambdaHack.Common.Flavour: instance Show Flavour
+ Game.LambdaHack.Common.Flavour: stdFlav :: [Flavour]
+ Game.LambdaHack.Common.Flavour: zipFancy :: [Color] -> [Flavour]
+ Game.LambdaHack.Common.Flavour: zipPlain :: [Color] -> [Flavour]
+ Game.LambdaHack.Common.HighScore: data ScoreTable
+ Game.LambdaHack.Common.HighScore: empty :: ScoreTable
+ Game.LambdaHack.Common.HighScore: instance Binary ScoreRecord
+ Game.LambdaHack.Common.HighScore: instance Binary ScoreTable
+ Game.LambdaHack.Common.HighScore: instance Eq ScoreRecord
+ Game.LambdaHack.Common.HighScore: instance Eq ScoreTable
+ Game.LambdaHack.Common.HighScore: instance Ord ScoreRecord
+ Game.LambdaHack.Common.HighScore: instance Show ScoreTable
+ Game.LambdaHack.Common.HighScore: register :: ScoreTable -> Int -> Time -> Status -> ClockTime -> Maybe (ScoreTable, Int)
+ Game.LambdaHack.Common.HighScore: slideshow :: ScoreTable -> Int -> Status -> Slideshow
+ Game.LambdaHack.Common.Item: Item :: !ItemKindIx -> !Char -> !Text -> !Flavour -> !(Effect Int) -> Item
+ Game.LambdaHack.Common.Item: buildItem :: FlavourMap -> DiscoRev -> Id ItemKind -> ItemKind -> Effect Int -> Item
+ Game.LambdaHack.Common.Item: data FlavourMap
+ Game.LambdaHack.Common.Item: data Item
+ Game.LambdaHack.Common.Item: data ItemId
+ Game.LambdaHack.Common.Item: data ItemKindIx
+ Game.LambdaHack.Common.Item: dungeonFlavourMap :: Ops ItemKind -> Rnd FlavourMap
+ Game.LambdaHack.Common.Item: emptyFlavourMap :: FlavourMap
+ Game.LambdaHack.Common.Item: instance Binary FlavourMap
+ Game.LambdaHack.Common.Item: instance Binary Item
+ Game.LambdaHack.Common.Item: instance Binary ItemId
+ Game.LambdaHack.Common.Item: instance Binary ItemKindIx
+ Game.LambdaHack.Common.Item: instance Constructor C1_0Item
+ Game.LambdaHack.Common.Item: instance Datatype D1Item
+ Game.LambdaHack.Common.Item: instance Enum ItemId
+ Game.LambdaHack.Common.Item: instance Enum ItemKindIx
+ Game.LambdaHack.Common.Item: instance Eq Item
+ Game.LambdaHack.Common.Item: instance Eq ItemId
+ Game.LambdaHack.Common.Item: instance Eq ItemKindIx
+ Game.LambdaHack.Common.Item: instance Generic Item
+ Game.LambdaHack.Common.Item: instance Hashable Item
+ Game.LambdaHack.Common.Item: instance Hashable ItemKindIx
+ Game.LambdaHack.Common.Item: instance Ix ItemKindIx
+ Game.LambdaHack.Common.Item: instance Ord Item
+ Game.LambdaHack.Common.Item: instance Ord ItemId
+ Game.LambdaHack.Common.Item: instance Ord ItemKindIx
+ Game.LambdaHack.Common.Item: instance Selector S1_0_0Item
+ Game.LambdaHack.Common.Item: instance Selector S1_0_1Item
+ Game.LambdaHack.Common.Item: instance Selector S1_0_2Item
+ Game.LambdaHack.Common.Item: instance Selector S1_0_3Item
+ Game.LambdaHack.Common.Item: instance Selector S1_0_4Item
+ Game.LambdaHack.Common.Item: instance Show FlavourMap
+ Game.LambdaHack.Common.Item: instance Show Item
+ Game.LambdaHack.Common.Item: instance Show ItemId
+ Game.LambdaHack.Common.Item: instance Show ItemKindIx
+ Game.LambdaHack.Common.Item: jeffect :: Item -> !(Effect Int)
+ Game.LambdaHack.Common.Item: jflavour :: Item -> !Flavour
+ Game.LambdaHack.Common.Item: jkind :: Discovery -> Item -> Maybe (Id ItemKind)
+ Game.LambdaHack.Common.Item: jkindIx :: Item -> !ItemKindIx
+ Game.LambdaHack.Common.Item: jname :: Item -> !Text
+ Game.LambdaHack.Common.Item: jsymbol :: Item -> !Char
+ Game.LambdaHack.Common.Item: newItem :: Ops ItemKind -> FlavourMap -> DiscoRev -> Int -> Int -> Rnd (Item, Int, ItemKind)
+ Game.LambdaHack.Common.Item: partItem :: Ops ItemKind -> Discovery -> Item -> (Part, Part)
+ Game.LambdaHack.Common.Item: partItemAW :: Ops ItemKind -> Discovery -> Item -> Part
+ Game.LambdaHack.Common.Item: partItemWs :: Ops ItemKind -> Discovery -> Int -> Item -> Part
+ Game.LambdaHack.Common.Item: serverDiscos :: Ops ItemKind -> Rnd (Discovery, DiscoRev)
+ Game.LambdaHack.Common.Item: strongestRegen :: [(ItemId, Item)] -> Maybe (Int, (ItemId, Item))
+ Game.LambdaHack.Common.Item: strongestSearch :: [(ItemId, Item)] -> Maybe (Int, (ItemId, Item))
+ Game.LambdaHack.Common.Item: strongestSword :: COps -> [(ItemId, Item)] -> Maybe (Int, (ItemId, Item))
+ Game.LambdaHack.Common.Item: type DiscoRev = EnumMap (Id ItemKind) ItemKindIx
+ Game.LambdaHack.Common.Item: type Discovery = EnumMap ItemKindIx (Id ItemKind)
+ Game.LambdaHack.Common.Item: viewItem :: Item -> (Char, Color)
+ Game.LambdaHack.Common.Key: BackTab :: Key
+ Game.LambdaHack.Common.Key: Begin :: Key
+ Game.LambdaHack.Common.Key: Char :: !Char -> Key
+ Game.LambdaHack.Common.Key: Control :: Modifier
+ Game.LambdaHack.Common.Key: Down :: Key
+ Game.LambdaHack.Common.Key: End :: Key
+ Game.LambdaHack.Common.Key: Esc :: Key
+ Game.LambdaHack.Common.Key: Home :: Key
+ Game.LambdaHack.Common.Key: KM :: !Modifier -> !Key -> KM
+ Game.LambdaHack.Common.Key: KP :: !Char -> Key
+ Game.LambdaHack.Common.Key: Left :: Key
+ Game.LambdaHack.Common.Key: NoModifier :: Modifier
+ Game.LambdaHack.Common.Key: PgDn :: Key
+ Game.LambdaHack.Common.Key: PgUp :: Key
+ Game.LambdaHack.Common.Key: Return :: Key
+ Game.LambdaHack.Common.Key: Right :: Key
+ Game.LambdaHack.Common.Key: Space :: Key
+ Game.LambdaHack.Common.Key: Tab :: Key
+ Game.LambdaHack.Common.Key: Unknown :: !String -> Key
+ Game.LambdaHack.Common.Key: Up :: Key
+ Game.LambdaHack.Common.Key: data KM
+ Game.LambdaHack.Common.Key: data Key
+ Game.LambdaHack.Common.Key: data Modifier
+ Game.LambdaHack.Common.Key: dirAllMoveKey :: [Key]
+ Game.LambdaHack.Common.Key: handleDir :: X -> KM -> (Vector -> a) -> a -> a
+ Game.LambdaHack.Common.Key: instance Binary KM
+ Game.LambdaHack.Common.Key: instance Binary Key
+ Game.LambdaHack.Common.Key: instance Binary Modifier
+ Game.LambdaHack.Common.Key: instance Eq KM
+ Game.LambdaHack.Common.Key: instance Eq Key
+ Game.LambdaHack.Common.Key: instance Eq Modifier
+ Game.LambdaHack.Common.Key: instance Ord KM
+ Game.LambdaHack.Common.Key: instance Ord Key
+ Game.LambdaHack.Common.Key: instance Ord Modifier
+ Game.LambdaHack.Common.Key: instance Show KM
+ Game.LambdaHack.Common.Key: key :: KM -> !Key
+ Game.LambdaHack.Common.Key: keyTranslate :: String -> Key
+ Game.LambdaHack.Common.Key: modifier :: KM -> !Modifier
+ Game.LambdaHack.Common.Key: moveBinding :: (VectorXY -> a) -> (VectorXY -> a) -> [(KM, a)]
+ Game.LambdaHack.Common.Key: showKM :: KM -> Text
+ Game.LambdaHack.Common.Kind: (!) :: Ix i => Array i c -> i -> Id c
+ Game.LambdaHack.Common.Kind: (//) :: Ix i => Array i c -> [(i, Id c)] -> Array i c
+ Game.LambdaHack.Common.Kind: COps :: !(Ops ActorKind) -> !(Ops CaveKind) -> !(Ops FactionKind) -> !(Ops ItemKind) -> !(Ops PlaceKind) -> !(Ops RuleKind) -> !(Ops StrategyKind) -> !(Ops TileKind) -> COps
+ Game.LambdaHack.Common.Kind: Ops :: (Id a -> Char) -> (Id a -> Text) -> (Id a -> a) -> (Text -> Id a) -> (Text -> (a -> Bool) -> Rnd (Id a)) -> (forall b. (Id a -> a -> b -> b) -> b -> b) -> (Id a, Id a) -> Speedup a -> Ops a
+ Game.LambdaHack.Common.Kind: array :: Ix i => (i, i) -> [(i, Id c)] -> Array i c
+ Game.LambdaHack.Common.Kind: bounds :: Ix i => Array i c -> (i, i)
+ Game.LambdaHack.Common.Kind: coactor :: COps -> !(Ops ActorKind)
+ Game.LambdaHack.Common.Kind: cocave :: COps -> !(Ops CaveKind)
+ Game.LambdaHack.Common.Kind: cofact :: COps -> !(Ops FactionKind)
+ Game.LambdaHack.Common.Kind: coitem :: COps -> !(Ops ItemKind)
+ Game.LambdaHack.Common.Kind: coplace :: COps -> !(Ops PlaceKind)
+ Game.LambdaHack.Common.Kind: corule :: COps -> !(Ops RuleKind)
+ Game.LambdaHack.Common.Kind: costrat :: COps -> !(Ops StrategyKind)
+ Game.LambdaHack.Common.Kind: cotile :: COps -> !(Ops TileKind)
+ Game.LambdaHack.Common.Kind: createOps :: Show a => ContentDef a -> Ops a
+ Game.LambdaHack.Common.Kind: data Array i c
+ Game.LambdaHack.Common.Kind: data COps
+ Game.LambdaHack.Common.Kind: data Id c
+ Game.LambdaHack.Common.Kind: data Ops a
+ Game.LambdaHack.Common.Kind: foldlArray :: Ix i => (a -> Id c -> a) -> a -> Array i c -> a
+ Game.LambdaHack.Common.Kind: instance (Ix i, Binary i) => Binary (Array i c)
+ Game.LambdaHack.Common.Kind: instance (Ix i, Show i) => Show (Array i c)
+ Game.LambdaHack.Common.Kind: instance Binary (Id c)
+ Game.LambdaHack.Common.Kind: instance Enum (Id c)
+ Game.LambdaHack.Common.Kind: instance Eq (Id c)
+ Game.LambdaHack.Common.Kind: instance Eq COps
+ Game.LambdaHack.Common.Kind: instance Ix (Id c)
+ Game.LambdaHack.Common.Kind: instance Ix i => Eq (Array i c)
+ Game.LambdaHack.Common.Kind: instance Ord (Id c)
+ Game.LambdaHack.Common.Kind: instance Show (Id c)
+ Game.LambdaHack.Common.Kind: instance Show COps
+ Game.LambdaHack.Common.Kind: listArray :: Ix i => (i, i) -> [Id c] -> Array i c
+ Game.LambdaHack.Common.Kind: obounds :: Ops a -> (Id a, Id a)
+ Game.LambdaHack.Common.Kind: ofoldrWithKey :: Ops a -> forall b. (Id a -> a -> b -> b) -> b -> b
+ Game.LambdaHack.Common.Kind: okind :: Ops a -> Id a -> a
+ Game.LambdaHack.Common.Kind: oname :: Ops a -> Id a -> Text
+ Game.LambdaHack.Common.Kind: opick :: Ops a -> Text -> (a -> Bool) -> Rnd (Id a)
+ Game.LambdaHack.Common.Kind: ospeedup :: Ops a -> Speedup a
+ Game.LambdaHack.Common.Kind: osymbol :: Ops a -> Id a -> Char
+ Game.LambdaHack.Common.Kind: ouniqGroup :: Ops a -> Text -> Id a
+ Game.LambdaHack.Common.Kind: sentinelId :: Id c
+ Game.LambdaHack.Common.Kind: stdRuleset :: Ops RuleKind -> RuleKind
+ Game.LambdaHack.Common.Level: CActor :: ActorId -> InvChar -> Container
+ Game.LambdaHack.Common.Level: CFloor :: LevelId -> Point -> Container
+ Game.LambdaHack.Common.Level: Level :: !Int -> !ActorPrio -> !ItemFloor -> !TileMap -> !X -> !Y -> !SmellMap -> !Text -> !(Point, Point) -> !Int -> !Int -> !Time -> !Int -> !Int -> !Int -> Level
+ Game.LambdaHack.Common.Level: accessible :: COps -> Level -> Point -> Point -> Bool
+ Game.LambdaHack.Common.Level: accessibleDir :: COps -> Level -> Point -> Vector -> Bool
+ Game.LambdaHack.Common.Level: ascendInBranch :: Dungeon -> LevelId -> Int -> [LevelId]
+ Game.LambdaHack.Common.Level: at :: Level -> Point -> Id TileKind
+ Game.LambdaHack.Common.Level: atI :: Level -> Point -> ItemBag
+ Game.LambdaHack.Common.Level: data Container
+ Game.LambdaHack.Common.Level: data Level
+ Game.LambdaHack.Common.Level: data LevelId
+ Game.LambdaHack.Common.Level: findPos :: TileMap -> (Point -> Id TileKind -> Bool) -> Rnd Point
+ Game.LambdaHack.Common.Level: findPosTry :: Int -> TileMap -> [Point -> Id TileKind -> Bool] -> Rnd Point
+ Game.LambdaHack.Common.Level: hideTile :: Ops TileKind -> Point -> Level -> Id TileKind
+ Game.LambdaHack.Common.Level: instance Binary Container
+ Game.LambdaHack.Common.Level: instance Binary Level
+ Game.LambdaHack.Common.Level: instance Constructor C1_0Container
+ Game.LambdaHack.Common.Level: instance Constructor C1_1Container
+ Game.LambdaHack.Common.Level: instance Datatype D1Container
+ Game.LambdaHack.Common.Level: instance Eq Container
+ Game.LambdaHack.Common.Level: instance Eq Level
+ Game.LambdaHack.Common.Level: instance Generic Container
+ Game.LambdaHack.Common.Level: instance Ord Container
+ Game.LambdaHack.Common.Level: instance Show Container
+ Game.LambdaHack.Common.Level: instance Show Level
+ Game.LambdaHack.Common.Level: lclear :: Level -> !Int
+ Game.LambdaHack.Common.Level: ldepth :: Level -> !Int
+ Game.LambdaHack.Common.Level: ldesc :: Level -> !Text
+ Game.LambdaHack.Common.Level: lfloor :: Level -> !ItemFloor
+ Game.LambdaHack.Common.Level: lhidden :: Level -> !Int
+ Game.LambdaHack.Common.Level: litemNum :: Level -> !Int
+ Game.LambdaHack.Common.Level: lprio :: Level -> !ActorPrio
+ Game.LambdaHack.Common.Level: lsecret :: Level -> !Int
+ Game.LambdaHack.Common.Level: lseen :: Level -> !Int
+ Game.LambdaHack.Common.Level: lsmell :: Level -> !SmellMap
+ Game.LambdaHack.Common.Level: lstair :: Level -> !(Point, Point)
+ Game.LambdaHack.Common.Level: ltile :: Level -> !TileMap
+ Game.LambdaHack.Common.Level: ltime :: Level -> !Time
+ Game.LambdaHack.Common.Level: lxsize :: Level -> !X
+ Game.LambdaHack.Common.Level: lysize :: Level -> !Y
+ Game.LambdaHack.Common.Level: mapDungeonActors_ :: Monad m => (ActorId -> m a) -> Dungeon -> m ()
+ Game.LambdaHack.Common.Level: mapLevelActors_ :: Monad m => (ActorId -> m a) -> Level -> m ()
+ Game.LambdaHack.Common.Level: type Dungeon = EnumMap LevelId Level
+ Game.LambdaHack.Common.Level: type ItemFloor = EnumMap Point ItemBag
+ Game.LambdaHack.Common.Level: type SmellMap = EnumMap Point SmellTime
+ Game.LambdaHack.Common.Level: type TileMap = Array Point TileKind
+ Game.LambdaHack.Common.Level: updateFloor :: (ItemFloor -> ItemFloor) -> Level -> Level
+ Game.LambdaHack.Common.Level: updatePrio :: (ActorPrio -> ActorPrio) -> Level -> Level
+ Game.LambdaHack.Common.Level: updateSmell :: (SmellMap -> SmellMap) -> Level -> Level
+ Game.LambdaHack.Common.Level: updateTile :: (TileMap -> TileMap) -> Level -> Level
+ Game.LambdaHack.Common.Misc: breturn :: MonadPlus m => Bool -> a -> m a
+ Game.LambdaHack.Common.Misc: data FactionId
+ Game.LambdaHack.Common.Misc: data LevelId
+ Game.LambdaHack.Common.Misc: divUp :: Int -> Int -> Int
+ Game.LambdaHack.Common.Misc: instance (Enum k, Binary k) => Binary (EnumSet k)
+ Game.LambdaHack.Common.Misc: instance (Enum k, Binary k, Binary e) => Binary (EnumMap k e)
+ Game.LambdaHack.Common.Misc: instance Binary FactionId
+ Game.LambdaHack.Common.Misc: instance Binary LevelId
+ Game.LambdaHack.Common.Misc: instance Enum FactionId
+ Game.LambdaHack.Common.Misc: instance Enum LevelId
+ Game.LambdaHack.Common.Misc: instance Enum k => Adjustable (EnumMap k)
+ Game.LambdaHack.Common.Misc: instance Enum k => FoldableWithKey (EnumMap k)
+ Game.LambdaHack.Common.Misc: instance Enum k => Indexable (EnumMap k)
+ Game.LambdaHack.Common.Misc: instance Enum k => Keyed (EnumMap k)
+ Game.LambdaHack.Common.Misc: instance Enum k => Lookup (EnumMap k)
+ Game.LambdaHack.Common.Misc: instance Enum k => TraversableWithKey (EnumMap k)
+ Game.LambdaHack.Common.Misc: instance Enum k => ZipWithKey (EnumMap k)
+ Game.LambdaHack.Common.Misc: instance Eq FactionId
+ Game.LambdaHack.Common.Misc: instance Eq LevelId
+ Game.LambdaHack.Common.Misc: instance Ord FactionId
+ Game.LambdaHack.Common.Misc: instance Ord LevelId
+ Game.LambdaHack.Common.Misc: instance Read LevelId
+ Game.LambdaHack.Common.Misc: instance Show FactionId
+ Game.LambdaHack.Common.Misc: instance Show LevelId
+ Game.LambdaHack.Common.Misc: instance Zip (EnumMap k)
+ Game.LambdaHack.Common.Misc: maxLevelDim :: Int
+ Game.LambdaHack.Common.Misc: normalLevelBound :: (Int, Int)
+ Game.LambdaHack.Common.Misc: type Freqs = [(Text, Int)]
+ Game.LambdaHack.Common.Msg: (<+>) :: Text -> Text -> Text
+ Game.LambdaHack.Common.Msg: (<>) :: Text -> Text -> Text
+ Game.LambdaHack.Common.Msg: addMsg :: Report -> Msg -> Report
+ Game.LambdaHack.Common.Msg: addReport :: Report -> History -> History
+ Game.LambdaHack.Common.Msg: data History
+ Game.LambdaHack.Common.Msg: data Report
+ Game.LambdaHack.Common.Msg: data Slideshow
+ Game.LambdaHack.Common.Msg: emptyHistory :: History
+ Game.LambdaHack.Common.Msg: emptyReport :: Report
+ Game.LambdaHack.Common.Msg: findInReport :: (ByteString -> Bool) -> Report -> Maybe ByteString
+ Game.LambdaHack.Common.Msg: instance Binary History
+ Game.LambdaHack.Common.Msg: instance Binary Report
+ Game.LambdaHack.Common.Msg: instance Monoid Slideshow
+ Game.LambdaHack.Common.Msg: instance Show History
+ Game.LambdaHack.Common.Msg: instance Show Report
+ Game.LambdaHack.Common.Msg: instance Show Slideshow
+ Game.LambdaHack.Common.Msg: makePhrase :: [Part] -> Text
+ Game.LambdaHack.Common.Msg: makeSentence :: [Part] -> Text
+ Game.LambdaHack.Common.Msg: mergeHistory :: [(Msg, History)] -> History
+ Game.LambdaHack.Common.Msg: moreMsg :: Msg
+ Game.LambdaHack.Common.Msg: nullReport :: Report -> Bool
+ Game.LambdaHack.Common.Msg: renderHistory :: History -> Overlay
+ Game.LambdaHack.Common.Msg: renderReport :: Report -> Text
+ Game.LambdaHack.Common.Msg: showT :: Show a => a -> Text
+ Game.LambdaHack.Common.Msg: singletonHistory :: Report -> History
+ Game.LambdaHack.Common.Msg: singletonReport :: Msg -> Report
+ Game.LambdaHack.Common.Msg: splitOverlay :: Y -> Overlay -> Overlay -> Slideshow
+ Game.LambdaHack.Common.Msg: splitReport :: Report -> [Text]
+ Game.LambdaHack.Common.Msg: stringByLocation :: X -> Y -> Overlay -> (Text, PointXY -> Maybe Char, Maybe Text)
+ Game.LambdaHack.Common.Msg: takeHistory :: Int -> History -> History
+ Game.LambdaHack.Common.Msg: toSlideshow :: [Overlay] -> Slideshow
+ Game.LambdaHack.Common.Msg: truncateMsg :: X -> Text -> Text
+ Game.LambdaHack.Common.Msg: type Msg = Text
+ Game.LambdaHack.Common.Msg: type Overlay = [Text]
+ Game.LambdaHack.Common.Msg: yesnoMsg :: Msg
+ Game.LambdaHack.Common.Perception: Perception :: PerActor -> PerceptionVisible -> PerceptionVisible -> Perception
+ Game.LambdaHack.Common.Perception: PerceptionVisible :: EnumSet Point -> PerceptionVisible
+ Game.LambdaHack.Common.Perception: actorSeesLoc :: Perception -> ActorId -> Point -> Bool
+ Game.LambdaHack.Common.Perception: addPer :: Perception -> Perception -> Perception
+ Game.LambdaHack.Common.Perception: data Perception
+ Game.LambdaHack.Common.Perception: diffPer :: Perception -> Perception -> Perception
+ Game.LambdaHack.Common.Perception: instance Binary Perception
+ Game.LambdaHack.Common.Perception: instance Binary PerceptionVisible
+ Game.LambdaHack.Common.Perception: instance Constructor C1_0Perception
+ Game.LambdaHack.Common.Perception: instance Datatype D1Perception
+ Game.LambdaHack.Common.Perception: instance Eq Perception
+ Game.LambdaHack.Common.Perception: instance Eq PerceptionVisible
+ Game.LambdaHack.Common.Perception: instance Generic Perception
+ Game.LambdaHack.Common.Perception: instance Selector S1_0_0Perception
+ Game.LambdaHack.Common.Perception: instance Selector S1_0_1Perception
+ Game.LambdaHack.Common.Perception: instance Selector S1_0_2Perception
+ Game.LambdaHack.Common.Perception: instance Show Perception
+ Game.LambdaHack.Common.Perception: instance Show PerceptionVisible
+ Game.LambdaHack.Common.Perception: newtype PerceptionVisible
+ Game.LambdaHack.Common.Perception: nullPer :: Perception -> Bool
+ Game.LambdaHack.Common.Perception: perActor :: Perception -> PerActor
+ Game.LambdaHack.Common.Perception: psmell :: Perception -> PerceptionVisible
+ Game.LambdaHack.Common.Perception: ptotal :: Perception -> PerceptionVisible
+ Game.LambdaHack.Common.Perception: pvisible :: PerceptionVisible -> EnumSet Point
+ Game.LambdaHack.Common.Perception: smellFromActors :: COps -> State -> PerActor -> PerceptionVisible
+ Game.LambdaHack.Common.Perception: smellVisible :: Perception -> EnumSet Point
+ Game.LambdaHack.Common.Perception: totalVisible :: Perception -> EnumSet Point
+ Game.LambdaHack.Common.Perception: type FactionPers = EnumMap LevelId Perception
+ Game.LambdaHack.Common.Perception: type PerActor = EnumMap ActorId PerceptionVisible
+ Game.LambdaHack.Common.Perception: type Pers = EnumMap FactionId FactionPers
+ Game.LambdaHack.Common.Point: adjacent :: X -> Point -> Point -> Bool
+ Game.LambdaHack.Common.Point: bla :: X -> Y -> Int -> Point -> Point -> Maybe [Point]
+ Game.LambdaHack.Common.Point: chessDist :: X -> Point -> Point -> Int
+ Game.LambdaHack.Common.Point: data Point
+ Game.LambdaHack.Common.Point: displacementXYZ :: X -> Point -> Point -> VectorXY
+ Game.LambdaHack.Common.Point: inside :: X -> Point -> Area -> Bool
+ Game.LambdaHack.Common.Point: instance Binary Point
+ Game.LambdaHack.Common.Point: instance Enum Point
+ Game.LambdaHack.Common.Point: instance Eq Point
+ Game.LambdaHack.Common.Point: instance Ix Point
+ Game.LambdaHack.Common.Point: instance Ord Point
+ Game.LambdaHack.Common.Point: instance Random Point
+ Game.LambdaHack.Common.Point: instance Show Point
+ Game.LambdaHack.Common.Point: origin :: Point
+ Game.LambdaHack.Common.Point: showPoint :: X -> Point -> Text
+ Game.LambdaHack.Common.Point: toPoint :: X -> PointXY -> Point
+ Game.LambdaHack.Common.Point: vicinity :: X -> Y -> Point -> [Point]
+ Game.LambdaHack.Common.Point: vicinityCardinal :: X -> Y -> Point -> [Point]
+ Game.LambdaHack.Common.PointXY: PointXY :: (X, Y) -> PointXY
+ Game.LambdaHack.Common.PointXY: blaXY :: Int -> PointXY -> PointXY -> [PointXY]
+ Game.LambdaHack.Common.PointXY: fromTo :: PointXY -> PointXY -> [PointXY]
+ Game.LambdaHack.Common.PointXY: instance Enum PointXY
+ Game.LambdaHack.Common.PointXY: instance Eq PointXY
+ Game.LambdaHack.Common.PointXY: instance Ord PointXY
+ Game.LambdaHack.Common.PointXY: instance Show PointXY
+ Game.LambdaHack.Common.PointXY: newtype PointXY
+ Game.LambdaHack.Common.PointXY: sortPointXY :: (PointXY, PointXY) -> (PointXY, PointXY)
+ Game.LambdaHack.Common.PointXY: type X = Int
+ Game.LambdaHack.Common.PointXY: type Y = Int
+ Game.LambdaHack.Common.Random: RollDice :: Word8 -> Word8 -> RollDice
+ Game.LambdaHack.Common.Random: RollDiceXY :: (RollDice, RollDice) -> RollDiceXY
+ Game.LambdaHack.Common.Random: chance :: Chance -> Rnd Bool
+ Game.LambdaHack.Common.Random: chanceDeep :: Int -> Int -> RollDeep -> Rnd Bool
+ Game.LambdaHack.Common.Random: data RollDice
+ Game.LambdaHack.Common.Random: data RollDiceXY
+ Game.LambdaHack.Common.Random: frequency :: Show a => Frequency a -> Rnd a
+ Game.LambdaHack.Common.Random: instance Binary RollDice
+ Game.LambdaHack.Common.Random: instance Constructor C1_0RollDice
+ Game.LambdaHack.Common.Random: instance Datatype D1RollDice
+ Game.LambdaHack.Common.Random: instance Eq RollDice
+ Game.LambdaHack.Common.Random: instance Generic RollDice
+ Game.LambdaHack.Common.Random: instance Hashable RollDice
+ Game.LambdaHack.Common.Random: instance Ord RollDice
+ Game.LambdaHack.Common.Random: instance Read RollDice
+ Game.LambdaHack.Common.Random: instance Show RollDice
+ Game.LambdaHack.Common.Random: instance Show RollDiceXY
+ Game.LambdaHack.Common.Random: intToDeep :: Int -> RollDeep
+ Game.LambdaHack.Common.Random: maxDeep :: RollDeep -> Int
+ Game.LambdaHack.Common.Random: maxDice :: RollDice -> Int
+ Game.LambdaHack.Common.Random: meanDice :: RollDice -> Rational
+ Game.LambdaHack.Common.Random: minDice :: RollDice -> Int
+ Game.LambdaHack.Common.Random: oneOf :: [a] -> Rnd a
+ Game.LambdaHack.Common.Random: random :: Random a => Rnd a
+ Game.LambdaHack.Common.Random: randomR :: Random a => (a, a) -> Rnd a
+ Game.LambdaHack.Common.Random: rndToIO :: Rnd a -> IO a
+ Game.LambdaHack.Common.Random: roll :: Int -> Rnd Int
+ Game.LambdaHack.Common.Random: rollDeep :: Int -> Int -> RollDeep -> Rnd Int
+ Game.LambdaHack.Common.Random: rollDice :: RollDice -> Rnd Int
+ Game.LambdaHack.Common.Random: rollDiceXY :: RollDiceXY -> Rnd (Int, Int)
+ Game.LambdaHack.Common.Random: type Chance = Rational
+ Game.LambdaHack.Common.Random: type Rnd a = State StdGen a
+ Game.LambdaHack.Common.Random: type RollDeep = (RollDice, RollDice)
+ Game.LambdaHack.Common.ServerCmd: ApplySer :: ActorId -> ItemId -> Container -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: CfgDumpSer :: ActorId -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: DropSer :: ActorId -> ItemId -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: ExploreSer :: ActorId -> Vector -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: GameExitSer :: ActorId -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: GameRestartSer :: ActorId -> Text -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: GameSaveSer :: ActorId -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: MoveSer :: ActorId -> Vector -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: PickupSer :: ActorId -> ItemId -> Int -> InvChar -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: ProjectSer :: ActorId -> Point -> Int -> ItemId -> Container -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: RunSer :: ActorId -> Vector -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: SetPathSer :: ActorId -> [Vector] -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: TriggerSer :: ActorId -> Point -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: WaitSer :: ActorId -> CmdSer
+ Game.LambdaHack.Common.ServerCmd: aidCmdSer :: CmdSer -> ActorId
+ Game.LambdaHack.Common.ServerCmd: data CmdSer
+ Game.LambdaHack.Common.ServerCmd: instance Show CmdSer
+ Game.LambdaHack.Common.State: data State
+ Game.LambdaHack.Common.State: defStateGlobal :: Dungeon -> Int -> FactionDict -> COps -> ScoreTable -> State
+ Game.LambdaHack.Common.State: emptyState :: State
+ Game.LambdaHack.Common.State: getLocalTime :: LevelId -> State -> Time
+ Game.LambdaHack.Common.State: instance Binary State
+ Game.LambdaHack.Common.State: instance Eq State
+ Game.LambdaHack.Common.State: instance Show State
+ Game.LambdaHack.Common.State: isHumanFaction :: FactionId -> State -> Bool
+ Game.LambdaHack.Common.State: isSpawnFaction :: FactionId -> State -> Bool
+ Game.LambdaHack.Common.State: isSummonFaction :: FactionId -> State -> Bool
+ Game.LambdaHack.Common.State: localFromGlobal :: State -> State
+ Game.LambdaHack.Common.State: sactorD :: State -> ActorDict
+ Game.LambdaHack.Common.State: scops :: State -> COps
+ Game.LambdaHack.Common.State: sdepth :: State -> Int
+ Game.LambdaHack.Common.State: sdungeon :: State -> Dungeon
+ Game.LambdaHack.Common.State: sfactionD :: State -> FactionDict
+ Game.LambdaHack.Common.State: shigh :: State -> ScoreTable
+ Game.LambdaHack.Common.State: sitemD :: State -> ItemDict
+ Game.LambdaHack.Common.State: stime :: State -> Time
+ Game.LambdaHack.Common.State: updateActorD :: (ActorDict -> ActorDict) -> State -> State
+ Game.LambdaHack.Common.State: updateCOps :: (COps -> COps) -> State -> State
+ Game.LambdaHack.Common.State: updateDepth :: (Int -> Int) -> State -> State
+ Game.LambdaHack.Common.State: updateDungeon :: (Dungeon -> Dungeon) -> State -> State
+ Game.LambdaHack.Common.State: updateFaction :: (FactionDict -> FactionDict) -> State -> State
+ Game.LambdaHack.Common.State: updateItemD :: (ItemDict -> ItemDict) -> State -> State
+ Game.LambdaHack.Common.State: updateTime :: (Time -> Time) -> State -> State
+ Game.LambdaHack.Common.State: usesAIFaction :: FactionId -> State -> Bool
+ Game.LambdaHack.Common.Tile: changeTo :: Ops TileKind -> Id TileKind -> Rnd (Id TileKind)
+ Game.LambdaHack.Common.Tile: hasFeature :: Ops TileKind -> Feature -> Id TileKind -> Bool
+ Game.LambdaHack.Common.Tile: hiddenAs :: Ops TileKind -> Id TileKind -> Id TileKind
+ Game.LambdaHack.Common.Tile: isClear :: Ops TileKind -> Id TileKind -> Bool
+ Game.LambdaHack.Common.Tile: isExplorable :: Ops TileKind -> Id TileKind -> Bool
+ Game.LambdaHack.Common.Tile: isLit :: Ops TileKind -> Id TileKind -> Bool
+ Game.LambdaHack.Common.Tile: kindHas :: [Feature] -> [Feature] -> TileKind -> Bool
+ Game.LambdaHack.Common.Tile: kindHasFeature :: Feature -> TileKind -> Bool
+ Game.LambdaHack.Common.Tile: similar :: TileKind -> TileKind -> Bool
+ Game.LambdaHack.Common.Tile: speedup :: Bool -> Ops TileKind -> Speedup TileKind
+ Game.LambdaHack.Common.Tile: type SmellTime = Time
+ Game.LambdaHack.Common.Time: data Speed
+ Game.LambdaHack.Common.Time: data Time
+ Game.LambdaHack.Common.Time: instance Binary Speed
+ Game.LambdaHack.Common.Time: instance Binary Time
+ Game.LambdaHack.Common.Time: instance Enum Time
+ Game.LambdaHack.Common.Time: instance Eq Speed
+ Game.LambdaHack.Common.Time: instance Eq Time
+ Game.LambdaHack.Common.Time: instance Ord Speed
+ Game.LambdaHack.Common.Time: instance Ord Time
+ Game.LambdaHack.Common.Time: instance Show Speed
+ Game.LambdaHack.Common.Time: instance Show Time
+ Game.LambdaHack.Common.Time: rangeFromSpeed :: Speed -> Int
+ Game.LambdaHack.Common.Time: speedAdd :: Speed -> Speed -> Speed
+ Game.LambdaHack.Common.Time: speedFromWeight :: Int -> Int -> Speed
+ Game.LambdaHack.Common.Time: speedNegate :: Speed -> Speed
+ Game.LambdaHack.Common.Time: speedNormal :: Speed
+ Game.LambdaHack.Common.Time: speedScale :: Rational -> Speed -> Speed
+ Game.LambdaHack.Common.Time: speedZero :: Speed
+ Game.LambdaHack.Common.Time: ticksPerMeter :: Speed -> Time
+ Game.LambdaHack.Common.Time: timeAdd :: Time -> Time -> Time
+ Game.LambdaHack.Common.Time: timeClip :: Time
+ Game.LambdaHack.Common.Time: timeEpsilon :: Time
+ Game.LambdaHack.Common.Time: timeFit :: Time -> Time -> Int
+ Game.LambdaHack.Common.Time: timeNegate :: Time -> Time
+ Game.LambdaHack.Common.Time: timeScale :: Time -> Int -> Time
+ Game.LambdaHack.Common.Time: timeToDigit :: Time -> Time -> Char
+ Game.LambdaHack.Common.Time: timeTurn :: Time
+ Game.LambdaHack.Common.Time: timeZero :: Time
+ Game.LambdaHack.Common.Time: toSpeed :: Double -> Speed
+ Game.LambdaHack.Common.Time: traveled :: Speed -> Time -> Int
+ Game.LambdaHack.Common.Vector: data Vector
+ Game.LambdaHack.Common.Vector: diagonal :: X -> Vector -> Bool
+ Game.LambdaHack.Common.Vector: displacePath :: [Point] -> [Vector]
+ Game.LambdaHack.Common.Vector: displacement :: Point -> Point -> Vector
+ Game.LambdaHack.Common.Vector: euclidDistSq :: X -> Vector -> Vector -> Int
+ Game.LambdaHack.Common.Vector: instance Binary Vector
+ Game.LambdaHack.Common.Vector: instance Eq Vector
+ Game.LambdaHack.Common.Vector: instance Ord Vector
+ Game.LambdaHack.Common.Vector: instance Read Vector
+ Game.LambdaHack.Common.Vector: instance Show Vector
+ Game.LambdaHack.Common.Vector: isUnit :: X -> Vector -> Bool
+ Game.LambdaHack.Common.Vector: moves :: X -> [Vector]
+ Game.LambdaHack.Common.Vector: neg :: Vector -> Vector
+ Game.LambdaHack.Common.Vector: shift :: Point -> Vector -> Point
+ Game.LambdaHack.Common.Vector: shiftBounded :: X -> Area -> Point -> Vector -> Point
+ Game.LambdaHack.Common.Vector: shiftPath :: Point -> [Vector] -> [Point]
+ Game.LambdaHack.Common.Vector: toDir :: X -> VectorXY -> Vector
+ Game.LambdaHack.Common.Vector: toVector :: X -> VectorXY -> Vector
+ Game.LambdaHack.Common.Vector: towards :: X -> Point -> Point -> Vector
+ Game.LambdaHack.Common.VectorXY: VectorXY :: (X, Y) -> VectorXY
+ Game.LambdaHack.Common.VectorXY: chessDistXY :: VectorXY -> Int
+ Game.LambdaHack.Common.VectorXY: euclidDistSqXY :: VectorXY -> Int
+ Game.LambdaHack.Common.VectorXY: instance Binary VectorXY
+ Game.LambdaHack.Common.VectorXY: instance Eq VectorXY
+ Game.LambdaHack.Common.VectorXY: instance Ord VectorXY
+ Game.LambdaHack.Common.VectorXY: instance Read VectorXY
+ Game.LambdaHack.Common.VectorXY: instance Show VectorXY
+ Game.LambdaHack.Common.VectorXY: movesCardinalXY :: [VectorXY]
+ Game.LambdaHack.Common.VectorXY: movesXY :: [VectorXY]
+ Game.LambdaHack.Common.VectorXY: negXY :: VectorXY -> VectorXY
+ Game.LambdaHack.Common.VectorXY: newtype VectorXY
+ Game.LambdaHack.Common.VectorXY: shiftXY :: PointXY -> VectorXY -> PointXY
+ Game.LambdaHack.Content.CaveKind: cdefTile :: CaveKind -> Text
+ Game.LambdaHack.Content.CaveKind: chidden :: CaveKind -> Int
+ Game.LambdaHack.Content.FactionKind: fAiLeader :: FactionKind -> !Text
+ Game.LambdaHack.Content.FactionKind: fAiMember :: FactionKind -> !Text
+ Game.LambdaHack.Frontend: ConnMulti :: FromMulti -> ToMulti -> ConnMulti
+ Game.LambdaHack.Frontend: FrontFrame :: !AcFrame -> FrontReq
+ Game.LambdaHack.Frontend: FrontKey :: ![KM] -> !SingleFrame -> FrontReq
+ Game.LambdaHack.Frontend: FrontSlides :: [KM] -> [SingleFrame] -> FrontReq
+ Game.LambdaHack.Frontend: connMulti :: ConnMulti
+ Game.LambdaHack.Frontend: data ConnMulti
+ Game.LambdaHack.Frontend: data FrontReq
+ Game.LambdaHack.Frontend: fromMulti :: ConnMulti -> FromMulti
+ Game.LambdaHack.Frontend: frontAc :: FrontReq -> !AcFrame
+ Game.LambdaHack.Frontend: frontClear :: FrontReq -> [KM]
+ Game.LambdaHack.Frontend: frontFr :: FrontReq -> !SingleFrame
+ Game.LambdaHack.Frontend: frontKM :: FrontReq -> ![KM]
+ Game.LambdaHack.Frontend: frontSlides :: FrontReq -> [SingleFrame]
+ Game.LambdaHack.Frontend: frontendName :: String
+ Game.LambdaHack.Frontend: getConfirmGeneric :: Monad m => ([KM] -> a -> m KM) -> [KM] -> a -> m Bool
+ Game.LambdaHack.Frontend: loopFrontend :: FrontendSession -> ConnMulti -> IO ()
+ Game.LambdaHack.Frontend: startupF :: String -> IO () -> IO ()
+ Game.LambdaHack.Frontend: toMulti :: ConnMulti -> ToMulti
+ Game.LambdaHack.Frontend: type ChanFrontend = TQueue KM
+ Game.LambdaHack.Frontend.Chosen: data FrontendSession
+ Game.LambdaHack.Frontend.Chosen: display :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()
+ Game.LambdaHack.Frontend.Chosen: frontendName :: String
+ Game.LambdaHack.Frontend.Chosen: promptGetAnyKey :: FrontendSession -> SingleFrame -> IO KM
+ Game.LambdaHack.Frontend.Chosen: startup :: String -> (FrontendSession -> IO ()) -> IO ()
+ Game.LambdaHack.Server: mainSer :: (MonadAtomic m, MonadConnServer m) => COps -> (m () -> IO ()) -> (COps -> ((FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ()) -> (FactionId -> ChanServer CmdClientAI -> IO ()) -> IO ()) -> IO ()) -> IO ()
+ Game.LambdaHack.Server.Action: class MonadServer m => MonadConnServer m
+ Game.LambdaHack.Server.Action: class MonadActionRO m => MonadServer m
+ Game.LambdaHack.Server.Action: deduceQuits :: (MonadAtomic m, MonadServer m) => Actor -> Status -> m ()
+ Game.LambdaHack.Server.Action: dumpCfg :: MonadServer m => FilePath -> m ()
+ Game.LambdaHack.Server.Action: fovMode :: MonadServer m => m FovMode
+ Game.LambdaHack.Server.Action: getPerFid :: MonadServer m => FactionId -> LevelId -> m Perception
+ Game.LambdaHack.Server.Action: getServer :: MonadServer m => m StateServer
+ Game.LambdaHack.Server.Action: getsServer :: MonadServer m => (StateServer -> a) -> m a
+ Game.LambdaHack.Server.Action: killAllClients :: (MonadAtomic m, MonadConnServer m) => m ()
+ Game.LambdaHack.Server.Action: mkConfigRules :: MonadServer m => Ops RuleKind -> m (Config, StdGen, StdGen)
+ Game.LambdaHack.Server.Action: modifyServer :: MonadServer m => (StateServer -> StateServer) -> m ()
+ Game.LambdaHack.Server.Action: putServer :: MonadServer m => StateServer -> m ()
+ Game.LambdaHack.Server.Action: resetFidPerception :: MonadServer m => FactionId -> LevelId -> m ()
+ Game.LambdaHack.Server.Action: restoreScore :: MonadServer m => Config -> m ScoreTable
+ Game.LambdaHack.Server.Action: revealItems :: (MonadAtomic m, MonadServer m) => Maybe FactionId -> Maybe Actor -> m ()
+ Game.LambdaHack.Server.Action: rndToAction :: MonadServer m => Rnd a -> m a
+ Game.LambdaHack.Server.Action: saveGameBkp :: MonadServer m => m ()
+ Game.LambdaHack.Server.Action: saveGameSer :: MonadServer m => m ()
+ Game.LambdaHack.Server.Action: sendQueryAI :: MonadConnServer m => FactionId -> ActorId -> m CmdSer
+ Game.LambdaHack.Server.Action: sendQueryUI :: MonadConnServer m => FactionId -> ActorId -> m CmdSer
+ Game.LambdaHack.Server.Action: sendUpdateAI :: MonadConnServer m => FactionId -> CmdClientAI -> m ()
+ Game.LambdaHack.Server.Action: sendUpdateUI :: MonadConnServer m => FactionId -> CmdClientUI -> m ()
+ Game.LambdaHack.Server.Action: speedupCOps :: Bool -> COps -> COps
+ Game.LambdaHack.Server.Action: tryRestore :: MonadServer m => COps -> m (Maybe (State, StateServer))
+ Game.LambdaHack.Server.Action: updateConn :: (MonadAtomic m, MonadConnServer m) => (FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ()) -> (FactionId -> ChanServer CmdClientAI -> IO ()) -> m ()
+ Game.LambdaHack.Server.Action: waitForChildren :: IO ()
+ Game.LambdaHack.Server.Action.ActionClass: class MonadServer m => MonadConnServer m
+ Game.LambdaHack.Server.Action.ActionClass: class MonadActionRO m => MonadServer m
+ Game.LambdaHack.Server.Action.ActionClass: getDict :: MonadConnServer m => m ConnServerDict
+ Game.LambdaHack.Server.Action.ActionClass: getServer :: MonadServer m => m StateServer
+ Game.LambdaHack.Server.Action.ActionClass: getsDict :: MonadConnServer m => (ConnServerDict -> a) -> m a
+ Game.LambdaHack.Server.Action.ActionClass: getsServer :: MonadServer m => (StateServer -> a) -> m a
+ Game.LambdaHack.Server.Action.ActionClass: liftIO :: MonadServer m => IO a -> m a
+ Game.LambdaHack.Server.Action.ActionClass: modifyDict :: MonadConnServer m => (ConnServerDict -> ConnServerDict) -> m ()
+ Game.LambdaHack.Server.Action.ActionClass: modifyServer :: MonadServer m => (StateServer -> StateServer) -> m ()
+ Game.LambdaHack.Server.Action.ActionClass: putDict :: MonadConnServer m => ConnServerDict -> m ()
+ Game.LambdaHack.Server.Action.ActionClass: putServer :: MonadServer m => StateServer -> m ()
+ Game.LambdaHack.Server.Action.ActionType: data ActionSer a
+ Game.LambdaHack.Server.Action.ActionType: executorSer :: ActionSer () -> IO ()
+ Game.LambdaHack.Server.Action.ActionType: instance Functor ActionSer
+ Game.LambdaHack.Server.Action.ActionType: instance Monad ActionSer
+ Game.LambdaHack.Server.Action.ActionType: instance MonadAction ActionSer
+ Game.LambdaHack.Server.Action.ActionType: instance MonadActionRO ActionSer
+ Game.LambdaHack.Server.Action.ActionType: instance MonadConnServer ActionSer
+ Game.LambdaHack.Server.Action.ActionType: instance MonadServer ActionSer
+ Game.LambdaHack.Server.Action.ConfigIO: dump :: Config -> FilePath -> IO ()
+ Game.LambdaHack.Server.Action.ConfigIO: instance Show CP
+ Game.LambdaHack.Server.Action.ConfigIO: mkConfigRules :: Ops RuleKind -> IO (Config, StdGen, StdGen)
+ Game.LambdaHack.Server.Action.Save: restoreGameSer :: Config -> (FilePath -> IO FilePath) -> IO (Maybe (State, StateServer))
+ Game.LambdaHack.Server.Action.Save: saveGameBkpSer :: Config -> State -> StateServer -> IO ()
+ Game.LambdaHack.Server.Action.Save: saveGameSer :: Config -> State -> StateServer -> IO ()
+ Game.LambdaHack.Server.AtomicSemSer: atomicSendSem :: (MonadAction m, MonadConnServer m) => Atomic -> m ()
+ Game.LambdaHack.Server.Config: Config :: !String -> !(Map Text Caves) -> !Bool -> !FovMode -> !Int -> !FilePath -> !FilePath -> !FilePath -> ![(Int, Text)] -> !(Map Text Players) -> !(Map Text Scenario) -> Config
+ Game.LambdaHack.Server.Config: Player :: Text -> Text -> Int -> LevelId -> Player
+ Game.LambdaHack.Server.Config: Players :: [Player] -> [Player] -> [(Text, Text)] -> [(Text, Text)] -> Players
+ Game.LambdaHack.Server.Config: Scenario :: Text -> Text -> Scenario
+ Game.LambdaHack.Server.Config: configAppDataDir :: Config -> !FilePath
+ Game.LambdaHack.Server.Config: configCaves :: Config -> !(Map Text Caves)
+ Game.LambdaHack.Server.Config: configFirstDeathEnds :: Config -> !Bool
+ Game.LambdaHack.Server.Config: configFovMode :: Config -> !FovMode
+ Game.LambdaHack.Server.Config: configHeroNames :: Config -> ![(Int, Text)]
+ Game.LambdaHack.Server.Config: configPlayers :: Config -> !(Map Text Players)
+ Game.LambdaHack.Server.Config: configRulesCfgFile :: Config -> !FilePath
+ Game.LambdaHack.Server.Config: configSaveBkpClips :: Config -> !Int
+ Game.LambdaHack.Server.Config: configScenario :: Config -> !(Map Text Scenario)
+ Game.LambdaHack.Server.Config: configScoresFile :: Config -> !FilePath
+ Game.LambdaHack.Server.Config: configSelfString :: Config -> !String
+ Game.LambdaHack.Server.Config: data Config
+ Game.LambdaHack.Server.Config: data Player
+ Game.LambdaHack.Server.Config: data Players
+ Game.LambdaHack.Server.Config: data Scenario
+ Game.LambdaHack.Server.Config: instance Binary Config
+ Game.LambdaHack.Server.Config: instance Binary Player
+ Game.LambdaHack.Server.Config: instance Binary Players
+ Game.LambdaHack.Server.Config: instance Binary Scenario
+ Game.LambdaHack.Server.Config: instance NFData Config
+ Game.LambdaHack.Server.Config: instance NFData Player
+ Game.LambdaHack.Server.Config: instance NFData Players
+ Game.LambdaHack.Server.Config: instance NFData Scenario
+ Game.LambdaHack.Server.Config: instance Read Player
+ Game.LambdaHack.Server.Config: instance Read Players
+ Game.LambdaHack.Server.Config: instance Read Scenario
+ Game.LambdaHack.Server.Config: instance Show Config
+ Game.LambdaHack.Server.Config: instance Show Player
+ Game.LambdaHack.Server.Config: instance Show Players
+ Game.LambdaHack.Server.Config: instance Show Scenario
+ Game.LambdaHack.Server.Config: playerEntry :: Player -> LevelId
+ Game.LambdaHack.Server.Config: playerInitial :: Player -> Int
+ Game.LambdaHack.Server.Config: playerKind :: Player -> Text
+ Game.LambdaHack.Server.Config: playerName :: Player -> Text
+ Game.LambdaHack.Server.Config: playersAlly :: Players -> [(Text, Text)]
+ Game.LambdaHack.Server.Config: playersComputer :: Players -> [Player]
+ Game.LambdaHack.Server.Config: playersEnemy :: Players -> [(Text, Text)]
+ Game.LambdaHack.Server.Config: playersHuman :: Players -> [Player]
+ Game.LambdaHack.Server.Config: scenarioDungeon :: Scenario -> Text
+ Game.LambdaHack.Server.Config: scenarioPlayers :: Scenario -> Text
+ Game.LambdaHack.Server.Config: type Caves = EnumMap LevelId (Text, Bool)
+ Game.LambdaHack.Server.DungeonGen: FreshDungeon :: !Dungeon -> !Int -> FreshDungeon
+ Game.LambdaHack.Server.DungeonGen: data FreshDungeon
+ Game.LambdaHack.Server.DungeonGen: dungeonGen :: COps -> Caves -> Rnd FreshDungeon
+ Game.LambdaHack.Server.DungeonGen: freshDepth :: FreshDungeon -> !Int
+ Game.LambdaHack.Server.DungeonGen: freshDungeon :: FreshDungeon -> !Dungeon
+ Game.LambdaHack.Server.DungeonGen.AreaRnd: connectGrid :: (X, Y) -> Rnd [(PointXY, PointXY)]
+ Game.LambdaHack.Server.DungeonGen.AreaRnd: connectPlaces :: Area -> Area -> Rnd Corridor
+ Game.LambdaHack.Server.DungeonGen.AreaRnd: mkRoom :: (X, Y) -> Area -> Rnd Area
+ Game.LambdaHack.Server.DungeonGen.AreaRnd: mkVoidRoom :: Area -> Rnd Area
+ Game.LambdaHack.Server.DungeonGen.AreaRnd: randomConnection :: (X, Y) -> Rnd (PointXY, PointXY)
+ Game.LambdaHack.Server.DungeonGen.AreaRnd: type Corridor = [PointXY]
+ Game.LambdaHack.Server.DungeonGen.AreaRnd: xyInArea :: Area -> Rnd PointXY
+ Game.LambdaHack.Server.DungeonGen.Cave: Cave :: !(Id CaveKind) -> TileMapXY -> ItemFloorXY -> [Place] -> Cave
+ Game.LambdaHack.Server.DungeonGen.Cave: buildCave :: COps -> Int -> Int -> Id CaveKind -> Rnd Cave
+ Game.LambdaHack.Server.DungeonGen.Cave: data Cave
+ Game.LambdaHack.Server.DungeonGen.Cave: ditem :: Cave -> ItemFloorXY
+ Game.LambdaHack.Server.DungeonGen.Cave: dkind :: Cave -> !(Id CaveKind)
+ Game.LambdaHack.Server.DungeonGen.Cave: dmap :: Cave -> TileMapXY
+ Game.LambdaHack.Server.DungeonGen.Cave: dplaces :: Cave -> [Place]
+ Game.LambdaHack.Server.DungeonGen.Cave: instance Show Cave
+ Game.LambdaHack.Server.DungeonGen.Cave: type ItemFloorXY = EnumMap PointXY (Item, Int)
+ Game.LambdaHack.Server.DungeonGen.Cave: type TileMapXY = TileMapXY
+ Game.LambdaHack.Server.DungeonGen.Place: Place :: !(Id PlaceKind) -> !Area -> !Bool -> !Text -> !(Id TileKind) -> !(Id TileKind) -> Place
+ Game.LambdaHack.Server.DungeonGen.Place: buildFence :: Id TileKind -> Area -> TileMapXY
+ Game.LambdaHack.Server.DungeonGen.Place: buildPlace :: COps -> CaveKind -> Id TileKind -> Int -> Int -> Area -> Rnd (TileMapXY, Place)
+ Game.LambdaHack.Server.DungeonGen.Place: data Place
+ Game.LambdaHack.Server.DungeonGen.Place: instance Binary Place
+ Game.LambdaHack.Server.DungeonGen.Place: instance Show Place
+ Game.LambdaHack.Server.DungeonGen.Place: placeValid :: Area -> PlaceKind -> Bool
+ Game.LambdaHack.Server.DungeonGen.Place: qarea :: Place -> !Area
+ Game.LambdaHack.Server.DungeonGen.Place: qhollowFence :: Place -> !(Id TileKind)
+ Game.LambdaHack.Server.DungeonGen.Place: qkind :: Place -> !(Id PlaceKind)
+ Game.LambdaHack.Server.DungeonGen.Place: qlegend :: Place -> !Text
+ Game.LambdaHack.Server.DungeonGen.Place: qseen :: Place -> !Bool
+ Game.LambdaHack.Server.DungeonGen.Place: qsolidFence :: Place -> !(Id TileKind)
+ Game.LambdaHack.Server.DungeonGen.Place: type TileMapXY = EnumMap PointXY (Id TileKind)
+ Game.LambdaHack.Server.EffectSem: addHero :: (MonadAtomic m, MonadServer m) => FactionId -> Point -> LevelId -> [(Int, Text)] -> Maybe Int -> Time -> m ActorId
+ Game.LambdaHack.Server.EffectSem: createItems :: (MonadAtomic m, MonadServer m) => Int -> Point -> LevelId -> m ()
+ Game.LambdaHack.Server.EffectSem: deduceKilled :: (MonadAtomic m, MonadServer m) => Actor -> m ()
+ Game.LambdaHack.Server.EffectSem: effectSem :: (MonadAtomic m, MonadServer m) => Effect Int -> ActorId -> ActorId -> m Bool
+ Game.LambdaHack.Server.EffectSem: electLeader :: MonadAtomic m => FactionId -> LevelId -> ActorId -> m ()
+ Game.LambdaHack.Server.EffectSem: itemEffect :: (MonadAtomic m, MonadServer m) => ActorId -> ActorId -> Maybe ItemId -> Item -> m ()
+ Game.LambdaHack.Server.EffectSem: spawnMonsters :: (MonadAtomic m, MonadServer m) => [Point] -> LevelId -> ((FactionId, Faction) -> Bool) -> Time -> Text -> m ()
+ Game.LambdaHack.Server.Fov: Blind :: FovMode
+ Game.LambdaHack.Server.Fov: Digital :: Int -> FovMode
+ Game.LambdaHack.Server.Fov: Permissive :: FovMode
+ Game.LambdaHack.Server.Fov: Shadow :: FovMode
+ Game.LambdaHack.Server.Fov: data FovMode
+ Game.LambdaHack.Server.Fov: dungeonPerception :: COps -> FovMode -> State -> Pers
+ Game.LambdaHack.Server.Fov: fullscan :: Ops TileKind -> FovMode -> Point -> Level -> [Point]
+ Game.LambdaHack.Server.Fov: instance Binary FovMode
+ Game.LambdaHack.Server.Fov: instance Read FovMode
+ Game.LambdaHack.Server.Fov: instance Show FovMode
+ Game.LambdaHack.Server.Fov: instance Show PerceptionReachable
+ Game.LambdaHack.Server.Fov: levelPerception :: COps -> State -> FovMode -> FactionId -> LevelId -> Level -> Perception
+ Game.LambdaHack.Server.Fov.Common: B :: (X, Y) -> Bump
+ Game.LambdaHack.Server.Fov.Common: addHull :: (Bump -> Bump -> Bool) -> Bump -> ConvexHull -> ConvexHull
+ Game.LambdaHack.Server.Fov.Common: instance Show Bump
+ Game.LambdaHack.Server.Fov.Common: maximal :: (a -> a -> Bool) -> [a] -> a
+ Game.LambdaHack.Server.Fov.Common: newtype Bump
+ Game.LambdaHack.Server.Fov.Common: steeper :: Bump -> Bump -> Bump -> Bool
+ Game.LambdaHack.Server.Fov.Common: type ConvexHull = [Bump]
+ Game.LambdaHack.Server.Fov.Common: type Distance = Int
+ Game.LambdaHack.Server.Fov.Common: type Edge = (Line, ConvexHull)
+ Game.LambdaHack.Server.Fov.Common: type EdgeInterval = (Edge, Edge)
+ Game.LambdaHack.Server.Fov.Common: type Line = (Bump, Bump)
+ Game.LambdaHack.Server.Fov.Common: type Progress = Int
+ Game.LambdaHack.Server.Fov.Digital: debugLine :: Line -> (Bool, String)
+ Game.LambdaHack.Server.Fov.Digital: debugSteeper :: Bump -> Bump -> Bump -> Bool
+ Game.LambdaHack.Server.Fov.Digital: dline :: Bump -> Bump -> Line
+ Game.LambdaHack.Server.Fov.Digital: dsteeper :: Bump -> Bump -> Bump -> Bool
+ Game.LambdaHack.Server.Fov.Digital: intersect :: Line -> Distance -> (Int, Int)
+ Game.LambdaHack.Server.Fov.Digital: scan :: Distance -> (Bump -> Bool) -> [Bump]
+ Game.LambdaHack.Server.Fov.Permissive: debugLine :: Line -> (Bool, String)
+ Game.LambdaHack.Server.Fov.Permissive: debugSteeper :: Bump -> Bump -> Bump -> Bool
+ Game.LambdaHack.Server.Fov.Permissive: dline :: Bump -> Bump -> Line
+ Game.LambdaHack.Server.Fov.Permissive: dsteeper :: Bump -> Bump -> Bump -> Bool
+ Game.LambdaHack.Server.Fov.Permissive: intersect :: Line -> Distance -> (Int, Int)
+ Game.LambdaHack.Server.Fov.Permissive: scan :: (Bump -> Bool) -> [Bump]
+ Game.LambdaHack.Server.Fov.Shadow: scan :: (SBump -> Bool) -> Distance -> Interval -> [SBump]
+ Game.LambdaHack.Server.Fov.Shadow: type Interval = (Rational, Rational)
+ Game.LambdaHack.Server.Fov.Shadow: type SBump = (Progress, Distance)
+ Game.LambdaHack.Server.LoopAction: loopSer :: (MonadAtomic m, MonadConnServer m) => DebugModeSer -> (CmdSer -> m Bool) -> (FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ()) -> (FactionId -> ChanServer CmdClientAI -> IO ()) -> COps -> m ()
+ Game.LambdaHack.Server.ServerSem: actorAttackActor :: (MonadAtomic m, MonadServer m) => ActorId -> ActorId -> m ()
+ Game.LambdaHack.Server.ServerSem: actorOpenDoor :: (MonadAtomic m, MonadServer m) => ActorId -> Vector -> Bool -> m Bool
+ Game.LambdaHack.Server.ServerSem: addProjectile :: (MonadAtomic m, MonadServer m) => ItemId -> Point -> LevelId -> FactionId -> [Point] -> Time -> m ActorId
+ Game.LambdaHack.Server.ServerSem: addSmell :: MonadAtomic m => ActorId -> m ()
+ Game.LambdaHack.Server.ServerSem: applySer :: (MonadAtomic m, MonadServer m) => ActorId -> ItemId -> Container -> m ()
+ Game.LambdaHack.Server.ServerSem: broadcastCmdAtomic :: MonadAtomic m => (FactionId -> CmdAtomic) -> m ()
+ Game.LambdaHack.Server.ServerSem: broadcastSfxAtomic :: MonadAtomic m => (FactionId -> SfxAtomic) -> m ()
+ Game.LambdaHack.Server.ServerSem: cfgDumpSer :: (MonadAtomic m, MonadServer m) => ActorId -> m ()
+ Game.LambdaHack.Server.ServerSem: displaceActor :: MonadAtomic m => ActorId -> ActorId -> m ()
+ Game.LambdaHack.Server.ServerSem: dropSer :: MonadAtomic m => ActorId -> ItemId -> m ()
+ Game.LambdaHack.Server.ServerSem: execFailure :: MonadAtomic m => FactionId -> Msg -> m Bool
+ Game.LambdaHack.Server.ServerSem: gameExitSer :: (MonadAtomic m, MonadServer m) => ActorId -> m ()
+ Game.LambdaHack.Server.ServerSem: gameRestartSer :: (MonadAtomic m, MonadServer m) => ActorId -> Text -> m ()
+ Game.LambdaHack.Server.ServerSem: gameSaveSer :: MonadServer m => m ()
+ Game.LambdaHack.Server.ServerSem: moveSer :: (MonadAtomic m, MonadServer m) => ActorId -> Vector -> Bool -> m Bool
+ Game.LambdaHack.Server.ServerSem: pickupSer :: MonadAtomic m => ActorId -> ItemId -> Int -> InvChar -> m ()
+ Game.LambdaHack.Server.ServerSem: projectSer :: (MonadAtomic m, MonadServer m) => ActorId -> Point -> Int -> ItemId -> Container -> m Bool
+ Game.LambdaHack.Server.ServerSem: runSer :: (MonadAtomic m, MonadServer m) => ActorId -> Vector -> m Bool
+ Game.LambdaHack.Server.ServerSem: setPathSer :: (MonadAtomic m, MonadServer m) => ActorId -> [Vector] -> m ()
+ Game.LambdaHack.Server.ServerSem: triggerSer :: (MonadAtomic m, MonadServer m) => ActorId -> Point -> m Bool
+ Game.LambdaHack.Server.ServerSem: waitSer :: MonadAtomic m => ActorId -> m ()
+ Game.LambdaHack.Server.StartAction: applyDebug :: MonadServer m => DebugModeSer -> m ()
+ Game.LambdaHack.Server.StartAction: gameReset :: MonadServer m => COps -> m State
+ Game.LambdaHack.Server.StartAction: initPer :: MonadServer m => m ()
+ Game.LambdaHack.Server.StartAction: reinitGame :: (MonadAtomic m, MonadServer m) => m ()
+ Game.LambdaHack.Server.State: DebugModeSer :: !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !(Maybe FovMode) -> !Bool -> DebugModeSer
+ Game.LambdaHack.Server.State: StateServer :: !Discovery -> !DiscoRev -> !ItemRev -> !FlavourMap -> !ActorId -> !ItemId -> ![Atomic] -> !Pers -> !StdGen -> !Text -> Config -> !Bool -> !Bool -> !DebugModeSer -> !DebugModeSer -> StateServer
+ Game.LambdaHack.Server.State: data DebugModeSer
+ Game.LambdaHack.Server.State: data StateServer
+ Game.LambdaHack.Server.State: defDebugModeSer :: DebugModeSer
+ Game.LambdaHack.Server.State: emptyStateServer :: StateServer
+ Game.LambdaHack.Server.State: instance Binary DebugModeSer
+ Game.LambdaHack.Server.State: instance Binary StateServer
+ Game.LambdaHack.Server.State: instance Show DebugModeSer
+ Game.LambdaHack.Server.State: instance Show StateServer
+ Game.LambdaHack.Server.State: sacounter :: StateServer -> !ActorId
+ Game.LambdaHack.Server.State: sallClear :: DebugModeSer -> !Bool
+ Game.LambdaHack.Server.State: sbkpSave :: StateServer -> !Bool
+ Game.LambdaHack.Server.State: scenario :: StateServer -> !Text
+ Game.LambdaHack.Server.State: sconfig :: StateServer -> Config
+ Game.LambdaHack.Server.State: sdebugCli :: DebugModeSer -> !Bool
+ Game.LambdaHack.Server.State: sdebugNxt :: StateServer -> !DebugModeSer
+ Game.LambdaHack.Server.State: sdebugSer :: StateServer -> !DebugModeSer
+ Game.LambdaHack.Server.State: sdisco :: StateServer -> !Discovery
+ Game.LambdaHack.Server.State: sdiscoRev :: StateServer -> !DiscoRev
+ Game.LambdaHack.Server.State: sflavour :: StateServer -> !FlavourMap
+ Game.LambdaHack.Server.State: sicounter :: StateServer -> !ItemId
+ Game.LambdaHack.Server.State: sitemRev :: StateServer -> !ItemRev
+ Game.LambdaHack.Server.State: sknowEvents :: DebugModeSer -> !Bool
+ Game.LambdaHack.Server.State: sknowMap :: DebugModeSer -> !Bool
+ Game.LambdaHack.Server.State: sniffIn :: DebugModeSer -> !Bool
+ Game.LambdaHack.Server.State: sniffOut :: DebugModeSer -> !Bool
+ Game.LambdaHack.Server.State: sper :: StateServer -> !Pers
+ Game.LambdaHack.Server.State: squit :: StateServer -> !Bool
+ Game.LambdaHack.Server.State: srandom :: StateServer -> !StdGen
+ Game.LambdaHack.Server.State: stryFov :: DebugModeSer -> !(Maybe FovMode)
+ Game.LambdaHack.Server.State: sundo :: StateServer -> ![Atomic]
+ Game.LambdaHack.Utils.Assert: skip :: Monad m => m ()
+ Game.LambdaHack.Utils.File: tryCopyDataFiles :: (FilePath -> IO FilePath) -> [(FilePath, FilePath)] -> IO ()
+ Game.LambdaHack.Utils.File: tryCreateDir :: FilePath -> IO ()
+ Game.LambdaHack.Utils.LQueue: dropStartLQueue :: LQueue (Maybe a) -> LQueue (Maybe a)
+ Game.LambdaHack.Utils.LQueue: lastLQueue :: LQueue (Maybe a) -> Maybe a
+ Game.LambdaHack.Utils.LQueue: lengthLQueue :: LQueue a -> Int
+ Game.LambdaHack.Utils.LQueue: toListLQueue :: LQueue a -> [a]
- Game.LambdaHack.Content.CaveKind: CaveKind :: Char -> Text -> Freqs -> X -> Y -> RollDiceXY -> RollDiceXY -> RollDeep -> Rational -> Chance -> Int -> Int -> Chance -> Chance -> Chance -> RollDice -> Text -> Text -> Text -> Text -> Text -> Text -> CaveKind
+ Game.LambdaHack.Content.CaveKind: CaveKind :: Char -> Text -> Freqs -> X -> Y -> RollDiceXY -> RollDiceXY -> RollDeep -> Rational -> Chance -> Int -> Int -> Chance -> Chance -> Int -> RollDice -> Text -> Text -> Text -> Text -> Text -> CaveKind
- Game.LambdaHack.Content.FactionKind: FactionKind :: !Char -> !Text -> !Freqs -> !Text -> !Text -> ![Text] -> ![Text] -> FactionKind
+ Game.LambdaHack.Content.FactionKind: FactionKind :: !Char -> !Text -> !Freqs -> !Text -> !Text -> FactionKind
- Game.LambdaHack.Content.ItemKind: ItemKind :: !Char -> !Text -> !Freqs -> ![Flavour] -> !Effect -> !RollDeep -> !RollDeep -> !Part -> !Part -> !Int -> !Int -> ItemKind
+ Game.LambdaHack.Content.ItemKind: ItemKind :: !Char -> !Text -> !Freqs -> ![Flavour] -> !(Effect RollDeep) -> !RollDeep -> !Part -> !Part -> !Int -> !Int -> ItemKind
- Game.LambdaHack.Content.ItemKind: ieffect :: ItemKind -> !Effect
+ Game.LambdaHack.Content.ItemKind: ieffect :: ItemKind -> !(Effect RollDeep)
Files
- Game/LambdaHack/Ability.hs +0/−20
- Game/LambdaHack/Action.hs +0/−460
- Game/LambdaHack/Action/ActionLift.hs +0/−214
- Game/LambdaHack/Action/ConfigIO.hs +0/−190
- Game/LambdaHack/Action/Frontend.hs +0/−33
- Game/LambdaHack/Action/Frontend/Chosen.hs +0/−21
- Game/LambdaHack/Action/Frontend/Curses.hs +0/−158
- Game/LambdaHack/Action/Frontend/Gtk.hs +0/−463
- Game/LambdaHack/Action/Frontend/Std.hs +0/−107
- Game/LambdaHack/Action/Frontend/Vty.hs +0/−128
- Game/LambdaHack/Action/HighScore.hs +0/−152
- Game/LambdaHack/Action/Save.hs +0/−143
- Game/LambdaHack/Actions.hs +0/−644
- Game/LambdaHack/Actor.hs +0/−198
- Game/LambdaHack/ActorState.hs +0/−307
- Game/LambdaHack/Animation.hs +0/−128
- Game/LambdaHack/Area.hs +0/−62
- Game/LambdaHack/AreaRnd.hs +0/−145
- Game/LambdaHack/Binding.hs +0/−87
- Game/LambdaHack/BindingAction.hs +0/−73
- Game/LambdaHack/CDefs.hs +0/−23
- Game/LambdaHack/Cave.hs +0/−206
- Game/LambdaHack/Client.hs +117/−0
- Game/LambdaHack/Client/Action.hs +428/−0
- Game/LambdaHack/Client/Action/ActionClass.hs +75/−0
- Game/LambdaHack/Client/Action/ActionType.hs +103/−0
- Game/LambdaHack/Client/Action/ConfigIO.hs +122/−0
- Game/LambdaHack/Client/Action/Save.hs +95/−0
- Game/LambdaHack/Client/AtomicSemCli.hs +661/−0
- Game/LambdaHack/Client/Binding.hs +119/−0
- Game/LambdaHack/Client/ClientSem.hs +208/−0
- Game/LambdaHack/Client/Config.hs +45/−0
- Game/LambdaHack/Client/Draw.hs +198/−0
- Game/LambdaHack/Client/HumanCmd.hs +154/−0
- Game/LambdaHack/Client/HumanGlobal.hs +432/−0
- Game/LambdaHack/Client/HumanLocal.hs +615/−0
- Game/LambdaHack/Client/HumanSem.hs +161/−0
- Game/LambdaHack/Client/LoopAction.hs +90/−0
- Game/LambdaHack/Client/RunAction.hs +209/−0
- Game/LambdaHack/Client/State.hs +206/−0
- Game/LambdaHack/Client/Strategy.hs +84/−0
- Game/LambdaHack/Client/StrategyAction.hs +405/−0
- Game/LambdaHack/Color.hs +0/−126
- Game/LambdaHack/Command.hs +0/−108
- Game/LambdaHack/CommandAction.hs +0/−73
- Game/LambdaHack/Common/Ability.hs +25/−0
- Game/LambdaHack/Common/Action.hs +49/−0
- Game/LambdaHack/Common/Actor.hs +274/−0
- Game/LambdaHack/Common/ActorState.hs +180/−0
- Game/LambdaHack/Common/Animation.hs +219/−0
- Game/LambdaHack/Common/Area.hs +62/−0
- Game/LambdaHack/Common/AtomicCmd.hs +185/−0
- Game/LambdaHack/Common/AtomicPos.hs +247/−0
- Game/LambdaHack/Common/AtomicSem.hs +409/−0
- Game/LambdaHack/Common/ClientCmd.hs +79/−0
- Game/LambdaHack/Common/Color.hs +138/−0
- Game/LambdaHack/Common/ContentDef.hs +23/−0
- Game/LambdaHack/Common/Effect.hs +120/−0
- Game/LambdaHack/Common/Faction.hs +168/−0
- Game/LambdaHack/Common/Feature.hs +39/−0
- Game/LambdaHack/Common/Flavour.hs +99/−0
- Game/LambdaHack/Common/HighScore.hs +159/−0
- Game/LambdaHack/Common/Item.hs +209/−0
- Game/LambdaHack/Common/Key.hs +232/−0
- Game/LambdaHack/Common/Kind.hs +185/−0
- Game/LambdaHack/Common/Level.hs +228/−0
- Game/LambdaHack/Common/Misc.hs +94/−0
- Game/LambdaHack/Common/Msg.hs +219/−0
- Game/LambdaHack/Common/Perception.hs +107/−0
- Game/LambdaHack/Common/Point.hs +113/−0
- Game/LambdaHack/Common/PointXY.hs +73/−0
- Game/LambdaHack/Common/Random.hs +165/−0
- Game/LambdaHack/Common/ServerCmd.hs +50/−0
- Game/LambdaHack/Common/State.hs +204/−0
- Game/LambdaHack/Common/Tile.hs +104/−0
- Game/LambdaHack/Common/Time.hs +160/−0
- Game/LambdaHack/Common/Vector.hs +161/−0
- Game/LambdaHack/Common/VectorXY.hs +42/−0
- Game/LambdaHack/Config.hs +0/−123
- Game/LambdaHack/Content/ActorKind.hs +7/−6
- Game/LambdaHack/Content/CaveKind.hs +24/−25
- Game/LambdaHack/Content/FactionKind.hs +6/−8
- Game/LambdaHack/Content/ItemKind.hs +12/−21
- Game/LambdaHack/Content/PlaceKind.hs +1/−1
- Game/LambdaHack/Content/RuleKind.hs +6/−6
- Game/LambdaHack/Content/StrategyKind.hs +2/−2
- Game/LambdaHack/Content/TileKind.hs +10/−6
- Game/LambdaHack/Draw.hs +0/−183
- Game/LambdaHack/Dungeon.hs +0/−85
- Game/LambdaHack/DungeonState.hs +0/−182
- Game/LambdaHack/Effect.hs +0/−61
- Game/LambdaHack/EffectAction.hs +0/−625
- Game/LambdaHack/FOV.hs +0/−68
- Game/LambdaHack/FOV/Common.hs +0/−67
- Game/LambdaHack/FOV/Digital.hs +0/−144
- Game/LambdaHack/FOV/Permissive.hs +0/−154
- Game/LambdaHack/FOV/Shadow.hs +0/−111
- Game/LambdaHack/Feature.hs +0/−32
- Game/LambdaHack/Flavour.hs +0/−95
- Game/LambdaHack/Frontend.hs +207/−0
- Game/LambdaHack/Frontend/Chosen.hs +21/−0
- Game/LambdaHack/Frontend/Curses.hs +158/−0
- Game/LambdaHack/Frontend/Gtk.hs +417/−0
- Game/LambdaHack/Frontend/Std.hs +107/−0
- Game/LambdaHack/Frontend/Vty.hs +129/−0
- Game/LambdaHack/Item.hs +0/−254
- Game/LambdaHack/ItemAction.hs +0/−525
- Game/LambdaHack/Key.hs +0/−209
- Game/LambdaHack/Kind.hs +0/−177
- Game/LambdaHack/Level.hs +0/−202
- Game/LambdaHack/Misc.hs +0/−25
- Game/LambdaHack/Msg.hs +0/−185
- Game/LambdaHack/Perception.hs +0/−194
- Game/LambdaHack/Place.hs +0/−184
- Game/LambdaHack/Point.hs +0/−102
- Game/LambdaHack/PointXY.hs +0/−60
- Game/LambdaHack/Random.hs +0/−143
- Game/LambdaHack/Running.hs +0/−194
- Game/LambdaHack/Server.hs +100/−0
- Game/LambdaHack/Server/Action.hs +382/−0
- Game/LambdaHack/Server/Action/ActionClass.hs +23/−0
- Game/LambdaHack/Server/Action/ActionType.hs +63/−0
- Game/LambdaHack/Server/Action/ConfigIO.hs +169/−0
- Game/LambdaHack/Server/Action/Save.hs +94/−0
- Game/LambdaHack/Server/AtomicSemSer.hs +180/−0
- Game/LambdaHack/Server/Config.hs +130/−0
- Game/LambdaHack/Server/DungeonGen.hs +135/−0
- Game/LambdaHack/Server/DungeonGen/AreaRnd.hs +150/−0
- Game/LambdaHack/Server/DungeonGen/Cave.hs +150/−0
- Game/LambdaHack/Server/DungeonGen/Place.hs +183/−0
- Game/LambdaHack/Server/EffectSem.hs +462/−0
- Game/LambdaHack/Server/Fov.hs +179/−0
- Game/LambdaHack/Server/Fov/Common.hs +67/−0
- Game/LambdaHack/Server/Fov/Digital.hs +142/−0
- Game/LambdaHack/Server/Fov/Permissive.hs +152/−0
- Game/LambdaHack/Server/Fov/Shadow.hs +111/−0
- Game/LambdaHack/Server/LoopAction.hs +406/−0
- Game/LambdaHack/Server/ServerSem.hs +445/−0
- Game/LambdaHack/Server/StartAction.hs +248/−0
- Game/LambdaHack/Server/State.hs +132/−0
- Game/LambdaHack/State.hs +0/−322
- Game/LambdaHack/Strategy.hs +0/−84
- Game/LambdaHack/StrategyAction.hs +0/−390
- Game/LambdaHack/Tile.hs +0/−96
- Game/LambdaHack/Time.hs +0/−143
- Game/LambdaHack/Turn.hs +0/−289
- Game/LambdaHack/Utils/Assert.hs +9/−23
- Game/LambdaHack/Utils/File.hs +24/−3
- Game/LambdaHack/Utils/Frequency.hs +7/−6
- Game/LambdaHack/Utils/LQueue.hs +32/−8
- Game/LambdaHack/Vector.hs +0/−163
- Game/LambdaHack/VectorXY.hs +0/−39
- LambdaHack.cabal +139/−101
- LambdaHack/Content/ActorKind.hs +14/−14
- LambdaHack/Content/CaveKind.hs +29/−13
- LambdaHack/Content/FactionKind.hs +25/−20
- LambdaHack/Content/ItemKind.hs +40/−50
- LambdaHack/Content/PlaceKind.hs +6/−6
- LambdaHack/Content/RuleKind.hs +15/−16
- LambdaHack/Content/StrategyKind.hs +6/−6
- LambdaHack/Content/TileKind.hs +67/−29
- LambdaHack/Main.hs +19/−9
- PLAYING.md +52/−50
- README.md +14/−25
- config.rules.bot +0/−9
- config.rules.default +33/−15
- config.ui.default +30/−45
- scores binary
− Game/LambdaHack/Ability.hs
@@ -1,20 +0,0 @@--- | AI strategy abilities.-module Game.LambdaHack.Ability- ( Ability(..)- ) where---- | All possible AI actor abilities. AI chooses among these when considering--- the next action to perform. The ability descriptions refer to the target--- 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- | Heal -- ^ heal if almost dead- | Flee -- ^ flee if almost dead- | Melee -- ^ melee target- | Pickup -- ^ gather items, if no foes visible- | Ranged -- ^ attack the visible target opponent at range, some of the time- | Tools -- ^ use items, if target opponent visible, some of the time- | Chase -- ^ chase the target, ignoring any actors on the way- | Wander -- ^ wander around, meleeing any opponents on the way- deriving (Show, Eq, Ord, Enum, Bounded)
− Game/LambdaHack/Action.hs
@@ -1,460 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Game action monad and basic building blocks for player and monster--- actions. Uses @liftIO@ of the @Action@ monad, but does not export it.--- Has no direct access to the Action monad implementation.-module Game.LambdaHack.Action- ( -- * Actions and accessors- Action, getPerception, getCOps, getBinding, getConfigUI- -- * Actions returning frames- , ActionFrame, returnNoFrame, returnFrame, whenFrame, inFrame, tryWithFrame- -- * Various ways to abort action- , abort, abortWith, abortIfWith, neverMind- -- * Abort exception handlers- , tryWith, tryRepeatedlyWith, tryIgnore- -- * Diary and report- , getDiary, msgAdd, recordHistory- -- * Key input- , getKeyCommand, getKeyFrameCommand, getOverConfirm- -- * Display each frame and confirm- , displayMore, displayYesNo, displayOverAbort- -- * Assorted frame operations- , displayOverlays, displayChoiceUI, displayFramePush, drawPrompt- -- * Clip init operations- , startClip, remember, rememberList- -- * Assorted primitives- , saveGameBkp, dumpCfg, endOrLoop, frontendName, startFrontend- , debug- ) where--import Control.Monad-import Control.Monad.State hiding (State, state, liftIO)-import qualified Data.IntMap as IM-import qualified Data.IntSet as IS-import qualified Data.Map as M-import System.Time-import Data.Maybe-import Control.Concurrent-import Control.Exception (finally)-import Data.Text (Text)-import qualified Data.Text as T--- import System.IO (hPutStrLn, stderr) -- just for debugging--import Game.LambdaHack.Action.ActionLift-import Game.LambdaHack.Perception-import Game.LambdaHack.Action.Frontend-import Game.LambdaHack.Draw-import Game.LambdaHack.Msg-import Game.LambdaHack.State-import Game.LambdaHack.Level-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import qualified Game.LambdaHack.Action.Save as Save-import qualified Game.LambdaHack.Kind as Kind-import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Binding-import Game.LambdaHack.Action.HighScore (register)-import Game.LambdaHack.Config-import qualified Game.LambdaHack.Action.ConfigIO as ConfigIO-import Game.LambdaHack.Animation (SingleFrame(..))-import Game.LambdaHack.Point-import qualified Game.LambdaHack.DungeonState as DungeonState-import Game.LambdaHack.Item-import Game.LambdaHack.Content.RuleKind-import qualified Game.LambdaHack.Tile as Tile---- | Set the current exception handler. Apart of executing it,--- draw and pass along a frame with the abort message, if any.-tryWithFrame :: Action a -> ActionFrame a -> ActionFrame a-tryWithFrame exc h =- let msgToFrames "" = returnNoFrame ()- msgToFrames msg = do- msgReset ""- fr <- drawPrompt ColorFull msg- returnFrame fr- excMsg msg = do- ((), frames) <- msgToFrames msg- a <- exc- return (a, frames)- in tryWith excMsg h---- | Store current report in the history and reset report.-recordHistory :: Action ()-recordHistory = do- Diary{sreport, shistory} <- getDiary- unless (nullReport sreport) $ do- ConfigUI{configHistoryMax} <- getConfigUI- msgReset ""- historyReset $ takeHistory configHistoryMax $ addReport sreport shistory---- | Wait for a player command.-getKeyCommand :: Maybe Bool -> Action (K.Key, K.Modifier)-getKeyCommand doPush = do- fs <- getFrontendSession- keyb <- getBinding- (nc, modifier) <- liftIO $ nextEvent fs doPush- return $ case modifier of- K.NoModifier -> (fromMaybe nc $ M.lookup nc $ kmacro keyb, modifier)- _ -> (nc, modifier)---- | Display frame and wait for a player command.-getKeyFrameCommand :: SingleFrame -> Action (K.Key, K.Modifier)-getKeyFrameCommand frame = do- fs <- getFrontendSession- keyb <- getBinding- (nc, modifier) <- liftIO $ promptGetKey fs [] frame- return $ case modifier of- K.NoModifier -> (fromMaybe nc $ M.lookup nc $ kmacro keyb, modifier)- _ -> (nc, modifier)---- | Ignore unexpected kestrokes until a SPACE or ESC is pressed.-getConfirm :: SingleFrame -> Action Bool-getConfirm frame = do- fs <- getFrontendSession- let keys = [ (K.Space, K.NoModifier), (K.Esc, K.NoModifier)]- (k, _) <- liftIO $ promptGetKey fs keys frame- case k of- K.Space -> return True- _ -> return False---- | A series of confirmations for all overlays.-getOverConfirm :: [SingleFrame] -> Action Bool-getOverConfirm [] = return True-getOverConfirm (x:xs) = do- b <- getConfirm x- if b- then getOverConfirm xs- else return False---- | A yes-no confirmation.-getYesNo :: SingleFrame -> Action Bool-getYesNo frame = do- fs <- getFrontendSession- let keys = [ (K.Char 'y', K.NoModifier)- , (K.Char 'n', K.NoModifier)- , (K.Esc, K.NoModifier)- ]- (k, _) <- liftIO $ promptGetKey fs keys frame- case k of- K.Char 'y' -> return True- _ -> return False---- TODO: perhaps add prompt to Report instead?-promptAdd :: Msg -> Msg -> Msg-promptAdd prompt msg = prompt <+> msg---- | Display a msg with a @more@ prompt. Return value indicates if the player--- tried to cancel/escape.-displayMore :: ColorMode -> Msg -> Action Bool-displayMore dm prompt = do- let newPrompt = promptAdd prompt moreMsg- frame <- drawPrompt dm newPrompt- getConfirm frame---- | 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 :: Msg -> Action Bool-displayYesNo prompt = do- frame <- drawPrompt ColorBW (promptAdd prompt yesnoMsg)- getYesNo frame---- | Print a msg and several overlays, one per page.--- All frames require confirmations. Raise @abort@ if the player presses ESC.-displayOverAbort :: Msg -> [Overlay] -> Action ()-displayOverAbort prompt xs = do- let newPrompt = promptAdd prompt ""- let f x = drawOverlay ColorFull newPrompt (x ++ [moreMsg])- frames <- mapM f xs- go <- getOverConfirm frames- when (not go) abort---- | Print a msg and several overlays, one per page.--- The last frame does not expect a confirmation and so does not show--- the invitation to press some keys.-displayOverlays :: Msg -> Msg -> [Overlay] -> ActionFrame ()-displayOverlays _ _ [] = returnNoFrame ()-displayOverlays prompt _ [x] = do- frame <- drawOverlay ColorFull prompt x- returnFrame frame-displayOverlays prompt pressKeys (x:xs) = do- frame <- drawOverlay ColorFull (promptAdd prompt pressKeys) (x ++ [moreMsg])- b <- getConfirm frame- if b- then displayOverlays prompt pressKeys xs- else returnNoFrame ()---- | 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 :: Msg -> [Overlay] -> [(K.Key, K.Modifier)]- -> Action (K.Key, K.Modifier)-displayChoiceUI prompt ovs keys = do- let (over, rest, spc, more, keysS) = case ovs of- [] -> ([], [], "", [], keys)- [x] -> (x, [], "", [], keys)- x:xs -> (x, xs, ", SPACE", [moreMsg], (K.Space, K.NoModifier) : keys)- legalKeys = (K.Esc, K.NoModifier) : keysS- frame <- drawOverlay ColorFull (prompt <> spc <> ", ESC]") (over ++ more)- fs <- getFrontendSession- (key, modifier) <- liftIO $ promptGetKey fs legalKeys frame- case key of- K.Esc -> neverMind True- K.Space | not (null rest) -> displayChoiceUI prompt rest keys- _ -> return (key, modifier)---- | Push a frame or a single frame's worth of delay to the frame queue.-displayFramePush :: Maybe SingleFrame -> Action ()-displayFramePush mframe = do- fs <- getFrontendSession- liftIO $ displayFrame fs False mframe---- | Draw the current level. The prompt is displayed, but not added--- to history. The prompt is appended to the current message--- and only the first screenful of the resulting overlay is displayed.-drawPrompt :: ColorMode -> Msg -> Action SingleFrame-drawPrompt dm prompt = do- cops <- getCOps- per <- getPerception- s <- get- Diary{sreport} <- getDiary- let over = splitReport $ addMsg sreport prompt- return $ draw dm cops per s over---- | Draw the current level. The prompt and the overlay are displayed,--- but not added to history. The prompt is appended to the current message--- and only the first line of the result is displayed.--- The overlay starts on the second line.-drawOverlay :: ColorMode -> Msg -> Overlay -> Action SingleFrame-drawOverlay dm prompt overlay = do- cops <- getCOps- per <- getPerception- s <- get- Diary{sreport} <- getDiary- let xsize = lxsize $ slevel s- msgPrompt = renderReport $ addMsg sreport prompt- over = padMsg xsize msgPrompt : overlay- return $ draw dm cops per s over---- | Initialize perception, etc., display level and run the action.-startClip :: Action () -> Action ()-startClip action =- -- Determine perception before running player command, in case monsters- -- have opened doors, etc.- withPerception $ do- remember -- heroes notice their surroundings, before they get displayed- displayPush -- draw the current surroundings- action -- let the actor act---- | Push the frame depicting the current level to the frame queue.--- Only one screenful of the report is shown, the rest is ignored.-displayPush :: Action ()-displayPush = do- fs <- getFrontendSession- s <- get- pl <- gets splayer- frame <- drawPrompt ColorFull ""- -- Visually speed up (by remving all empty frames) the show of the sequence- -- of the move frames if the player is running.- let (_, Actor{bdir}, _) = findActorAnyLevel pl s- isRunning = isJust bdir- liftIO $ displayFrame fs isRunning $ Just frame---- | Update heroes memory.-remember :: Action ()-remember = do- per <- getPerception- let vis = IS.toList (totalVisible per)- rememberList vis---- | Update heroes memory at the given list of locations.-rememberList :: [Point] -> Action ()-rememberList vis = do- Kind.COps{cotile=cotile@Kind.Ops{ouniqGroup}} <- getCOps- lvl <- gets slevel- let rememberTile = [(loc, lvl `at` loc) | loc <- vis]- unknownId = ouniqGroup "unknown space"- newClear (loc, tk) = lvl `rememberAt` loc == unknownId- && Tile.isExplorable cotile tk- clearN = length $ filter newClear rememberTile- modify (updateLevel (updateLRMap (Kind.// rememberTile)))- modify (updateLevel (\ l@Level{lseen} -> l {lseen = lseen + clearN}))- let alt Nothing = Nothing- alt (Just ([], _)) = Nothing- alt (Just (t, _)) = Just (t, t)- rememberItem = IM.alter alt- modify (updateLevel (updateIMap (\ m -> foldr rememberItem m vis)))---- | Save the diary and a backup of the save game file, in case of crashes.------ See 'Save.saveGameBkp'.-saveGameBkp :: Action ()-saveGameBkp = do- state <- get- diary <- getDiary- configUI <- getConfigUI- liftIO $ Save.saveGameBkp configUI state diary---- | Dumps the current game rules configuration to a file.-dumpCfg :: FilePath -> Action ()-dumpCfg fn = do- config <- gets sconfig- liftIO $ ConfigIO.dump config fn---- | Handle current score and display it with the high scores.--- Aborts if display of the scores was interrupted by the user.------ Warning: scores are shown during the game,--- so we should be careful not to leak secret information through them--- (e.g., the nature of the items through the total worth of inventory).-handleScores :: Bool -> Status -> Int -> Action ()-handleScores write status total =- when (total /= 0) $ do- configUI <- getConfigUI- time <- gets stime- curDate <- liftIO getClockTime- let score = register configUI write total time curDate status- (placeMsg, slideshow) <- liftIO score- displayOverAbort placeMsg slideshow---- | Continue or restart or exit the game.-endOrLoop :: Action () -> Action ()-endOrLoop handleTurn = do- squit <- gets squit- Kind.COps{coitem} <- getCOps- s <- get- configUI <- getConfigUI- let (_, total) = calculateTotal coitem s- -- The first, boolean component of squit determines- -- if ending screens should be shown, the other argument describes- -- the cause of the disruption of game flow.- case squit of- Nothing -> handleTurn -- just continue- Just (_, status@Camping) -> do- -- Save and display in parallel.- mv <- liftIO newEmptyMVar- liftIO $ void $ forkIO (Save.saveGameFile configUI s- `finally` putMVar mv ())- tryIgnore $ do- handleScores False status total- void $ displayMore ColorFull "See you soon, stronger and braver!"- liftIO $ takeMVar mv -- wait until saved- -- Do nothing, that is, quit the game loop.- Just (showScreens, status@Killed{}) -> do- Diary{sreport} <- getDiary- unless (nullReport sreport) $ do- -- Sisplay any leftover report. Suggest it could be the cause of death.- void $ displayMore ColorBW "Who would have thought?"- recordHistory -- prevent repeating the report- tryWith- (\ finalMsg ->- let highScoreMsg = "Let's hope another party can save the day!"- msg = if T.null finalMsg then highScoreMsg else finalMsg- in void $ displayMore ColorBW msg- -- Do nothing, that is, quit the game loop.- )- (do- when showScreens $ handleScores True status total- go <- displayMore ColorBW "Next time will be different."- when (not go) $ abortWith "You could really win this time."- restartGame handleTurn- )- Just (showScreens, status@Victor) -> do- Diary{sreport} <- getDiary- unless (nullReport sreport) $ do- -- Sisplay any leftover report. Suggest it could be the master move.- void $ displayMore ColorFull "Brilliant, wasn't it?"- recordHistory -- prevent repeating the report- when showScreens $ do- tryIgnore $ handleScores True status total- void $ displayMore ColorFull "Can it be done better, though?"- restartGame handleTurn- Just (_, Restart) -> do- void $ displayMore ColorBW "This time for real."- restartGame handleTurn--restartGame :: Action () -> Action ()-restartGame handleTurn = do- -- Take the original config from config file, to reroll RNG, if needed- -- (the current config file has the RNG rolled for the previous game).- configUI <- getConfigUI- cops <- getCOps- state <- gameResetAction configUI cops- modify $ const state- saveGameBkp- handleTurn---- TODO: do this inside Action ()-gameReset :: ConfigUI -> Kind.COps -> IO State-gameReset configUI cops@Kind.COps{ coitem- , corule- , cofact=Kind.Ops{opick}} = do- -- Rules config reloaded at each new game start.- (configRules, dungeonGen, startingGen) <- ConfigIO.mkConfigRules corule- let (DungeonState.FreshDungeon{..}, gen2) =- runState (DungeonState.generate cops configRules) dungeonGen- (sflavour, gen3) = runState (dungeonFlavourMap coitem) gen2- factionName = configFaction configRules- sfaction = evalState (opick factionName (const True)) gen3- let state = defaultState configRules sfaction sflavour freshDungeon- entryLevel entryLoc startingGen- hstate = initialHeroes cops entryLoc configUI state- return hstate-gameResetAction :: ConfigUI -> Kind.COps -> Action State-gameResetAction configUI cops = liftIO $ gameReset configUI cops---- | Wire together content, the definitions of game commands,--- config and a high-level startup function--- to form the starting game session. Evaluate to check for errors,--- in particular verify content consistency.--- Then create the starting game config from the default config file--- and initialize the engine with the starting session.-startFrontend :: Kind.COps -> (ConfigUI -> Binding (ActionFrame ()))- -> Action () -> IO ()-startFrontend !scops@Kind.COps{corule} stdBinding handleTurn = do- -- UI config reloaded at each client start.- sconfigUI <- ConfigIO.mkConfigUI corule- let !sbinding = stdBinding sconfigUI- font = configFont sconfigUI- -- In addition to handling the turn, if the game ends or exits,- -- handle the diary and backup savefile.- handleGame = do- handleTurn- diary <- getDiary- -- Save diary often, at each game exit, in case of crashes.- liftIO $ Save.rmBkpSaveDiary sconfigUI diary- loop sfs = start Session{..} handleGame- startup font loop---- | Compute and insert auxiliary optimized components into game content,--- to be used in time-critical sections of the code.-speedupCops :: Session -> Session-speedupCops sess@Session{scops = cops@Kind.COps{cotile=tile}} =- let ospeedup = Tile.speedup tile- cotile = tile {Kind.ospeedup}- scops = cops {Kind.cotile}- in sess {scops}---- | Either restore a saved game, or setup a new game.--- Then call the main game loop.-start :: Session -> Action () -> IO ()-start slowSess handleGame = do- let sess@Session{scops = cops@Kind.COps{corule}, sconfigUI} =- speedupCops slowSess- title = rtitle $ Kind.stdRuleset corule- pathsDataFile = rpathsDataFile $ Kind.stdRuleset corule- restored <- Save.restoreGame sconfigUI pathsDataFile title- case restored of- Right (diary, msg) -> do -- Starting a new game.- state <- gameReset sconfigUI cops- handlerToIO sess state- diary{sreport = singletonReport msg}- -- TODO: gameReset >> handleTurn or defaultState {squit=Reset}- handleGame- Left (state, diary, msg) -> -- Running a restored game.- handlerToIO sess state- -- This overwrites the "Really save/quit?" messages.- diary{sreport = singletonReport msg}- handleGame---- | Debugging.-debug :: Text -> Action ()-debug _x = return () -- liftIO $ hPutStrLn stderr _x
− Game/LambdaHack/Action/ActionLift.hs
@@ -1,214 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Game action monad and basic building blocks for player and monster--- actions. Exports 'liftIO' for injecting 'IO' into the 'Action' monad,--- but does not export the implementation of the Action monad.--- The 'liftIO' and 'handlerToIO' operations are used only in Action.hs.-{-# LANGUAGE MultiParamTypeClasses, RankNTypes #-}-module Game.LambdaHack.Action.ActionLift- ( -- * Actions and basic operations- ActionFun, Action, liftIO, handlerToIO, withPerception, getPerception- -- * Actions returning frames- , ActionFrame, returnNoFrame, returnFrame, whenFrame, inFrame- -- * Game session and assessors to its components- , Session(..), getFrontendSession, getCOps, getBinding, getConfigUI- -- * Various ways to abort action- , abort, abortWith, abortIfWith, neverMind- -- * Abort exception handlers- , tryWith, tryRepeatedlyWith, tryIgnore- -- * Diary and report- , getDiary, msgAdd, historyReset, msgReset- ) where--import Control.Monad.State hiding (State, state, liftIO)-import qualified Data.List as L-import Data.Maybe-import qualified Data.Text as T--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Perception-import Game.LambdaHack.Action.Frontend-import Game.LambdaHack.Msg-import Game.LambdaHack.State-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Binding-import Game.LambdaHack.Config-import Game.LambdaHack.Animation (SingleFrame(..))---- | The type of the function inside any action.--- (Separated from the @Action@ type to document each argument with haddock.)-type ActionFun r a =- Session -- ^ session setup data- -> DungeonPerception -- ^ cached perception- -> (State -> Diary -> a -> IO r) -- ^ continuation- -> (Msg -> IO r) -- ^ failure/reset continuation- -> State -- ^ current state- -> Diary -- ^ current diary- -> IO r---- | Actions of player-controlled characters and of any other actors.-newtype Action a = Action- { runAction :: forall r . ActionFun r a- }--instance Show (Action a) where- show _ = "an action"---- TODO: check if it's strict enough, if we don't keep old states for too long,--- Perhaps make state type fields strict for that, too?-instance Monad Action where- return = returnAction- (>>=) = bindAction--instance Functor Action where- fmap f (Action g) = Action (\ s p k a st ms ->- let k' st' ms' = k st' ms' . f- in g s p k' a st ms)--instance MonadState State Action where- get = Action (\ _s _p k _a st ms -> k st ms st)- put nst = Action (\ _s _p k _a _st ms -> k nst ms ())---- | Invokes the action continuation on the provided argument.-returnAction :: a -> Action a-returnAction x = Action (\ _s _p k _a st m -> k st m x)---- | Distributes the session and shutdown continuation,--- threads the state and diary.-bindAction :: Action a -> (a -> Action b) -> Action b-bindAction m f = Action (\ s p k a st ms ->- let next nst nm x =- runAction (f x) s p k a nst nm- in runAction m s p next a st ms)---- Instance commented out, so that outside of Action/ nobody can--- subvert the Action monad by invoking arbitrary IO.--- instance MonadIO Action where-liftIO :: IO a -> Action a-liftIO x = Action (\ _s _p k _a st ms -> x >>= k st ms)---- | Run an action, with a given session, state and diary, in the @IO@ monad.-handlerToIO :: Session -> State -> Diary -> Action () -> IO ()-handlerToIO sess@Session{scops} state diary h =- runAction h- sess- (dungeonPerception scops state) -- create and cache perception- (\ _ _ x -> return x) -- final continuation returns result- (\ msg -> -- e.g., in AI code- ioError $ userError $ T.unpack $ "unhandled abort:" <+> msg)- state- diary---- | Update the cached perception for the given computation.-withPerception :: Action () -> Action ()-withPerception h =- Action (\ sess@Session{scops} _ k a st ms ->- runAction h sess (dungeonPerception scops st) k a st ms)---- | Get the current perception.-getPerception :: Action Perception-getPerception = Action (\ _s per k _a s ms ->- k s ms (fromJust $ L.lookup (slid s) per))---- | Actions and screen frames, including delays, resulting--- from performing the actions.-type ActionFrame a = Action (a, [Maybe SingleFrame])---- | Return the value with an empty set of screen frames.-returnNoFrame :: a -> ActionFrame a-returnNoFrame a = return (a, [])---- | Return the trivial value with a single frame to show.-returnFrame :: SingleFrame -> ActionFrame ()-returnFrame fr = return ((), [Just fr])---- | As the @when@ monad operation, but on type @ActionFrame ()@.-whenFrame :: Bool -> ActionFrame () -> ActionFrame ()-whenFrame True x = x-whenFrame False _ = returnNoFrame ()---- | Inject action into actions with screen frames.-inFrame :: Action () -> ActionFrame ()-inFrame act = act >> returnNoFrame ()---- | The information that is constant across a playing session,--- including many consecutive games in a single session,--- but is completely disregarded and reset when a new playing session starts.-data Session = Session- { sfs :: FrontendSession -- ^ frontend session information- , scops :: Kind.COps -- ^ game content- , sbinding :: Binding (ActionFrame ()) -- ^ binding of keys to commands- , sconfigUI :: ConfigUI -- ^ the UI config for this session- }---- | Get the frontend session.-getFrontendSession :: Action FrontendSession-getFrontendSession = Action (\ Session{sfs} _p k _a st ms -> k st ms sfs)---- | Get the content operations.-getCOps :: Action Kind.COps-getCOps = Action (\ Session{scops} _p k _a st ms -> k st ms scops)---- | Get the key binding.-getBinding :: Action (Binding (ActionFrame ()))-getBinding = Action (\ Session{sbinding} _p k _a st ms -> k st ms sbinding)---- | Get the config from the config file.-getConfigUI :: Action ConfigUI-getConfigUI = Action (\ Session{sconfigUI} _p k _a st ms -> k st ms sconfigUI)---- | Reset the state and resume from the last backup point, i.e., invoke--- the failure continuation.-abort :: Action a-abort = abortWith ""---- | Abort with the given message.-abortWith :: Msg -> Action a-abortWith msg = Action (\ _s _p _k a _st _ms -> a msg)---- | Abort and print the given msg if the condition is true.-abortIfWith :: Bool -> Msg -> Action a-abortIfWith True msg = abortWith msg-abortIfWith False _ = abortWith ""---- | Abort and conditionally print the fixed message.-neverMind :: Bool -> Action a-neverMind b = abortIfWith b "never mind"---- | Set the current exception handler. First argument is the handler,--- second is the computation the handler scopes over.-tryWith :: (Msg -> Action a) -> Action a -> Action a-tryWith exc h = Action (\ s p k a st ms ->- let runA msg = runAction (exc msg) s p k a st ms- in runAction h s p k runA st ms)---- | Take a handler and a computation. If the computation fails, the--- handler is invoked and then the computation is retried.-tryRepeatedlyWith :: (Msg -> Action ()) -> Action () -> Action ()-tryRepeatedlyWith exc h =- tryWith (\ msg -> exc msg >> tryRepeatedlyWith exc h) h---- | Try the given computation and silently catch failure.-tryIgnore :: Action () -> Action ()-tryIgnore =- tryWith (\ msg -> if T.null msg- then return ()- else assert `failure` msg <+> "in tryIgnore")---- | Get the current diary.-getDiary :: Action Diary-getDiary = Action (\ _s _p k _a st diary -> k st diary diary)---- | Add a message to the current report.-msgAdd :: Msg -> Action ()-msgAdd nm = Action (\ _s _p k _a st ms ->- k st ms{sreport = addMsg (sreport ms) nm} ())---- | Wipe out and set a new value for the history.-historyReset :: History -> Action ()-historyReset shistory = Action (\ _s _p k _a st Diary{sreport} ->- k st Diary{..} ())---- | Wipe out and set a new value for the current report.-msgReset :: Msg -> Action ()-msgReset nm = Action (\ _s _p k _a st ms ->- k st ms{sreport = singletonReport nm} ())
− Game/LambdaHack/Action/ConfigIO.hs
@@ -1,190 +0,0 @@--- | Personal game configuration file support.-module Game.LambdaHack.Action.ConfigIO- ( mkConfigRules, mkConfigUI, dump- ) where--import System.Directory-import System.FilePath-import System.Environment-import qualified Data.ConfigFile as CF-import qualified Data.Char as Char-import Data.List-import qualified System.Random as R-import qualified Data.Text as T--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Config-import qualified Game.LambdaHack.Key as K-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Content.RuleKind--overrideCP :: CP -> FilePath -> IO CP-overrideCP cp@(CP defCF) cfile = do- b <- doesFileExist cfile- if not b- then return cp- else do- c <- CF.readfile defCF cfile- return $ toCP $ forceEither c---- | Read a player configuration file and use it to override--- options from a default config. Currently we can't unset options,--- only override. The default config, passed in argument @configDefault@,--- is expected to come from a default configuration file included via CPP.--- The player configuration comes from file @cfile@.-mkConfig :: String -> FilePath -> IO CP-mkConfig configDefault cfile = do- let delComment = map (drop 2) $ lines configDefault- unConfig = unlines delComment- -- Evaluate, to catch config errors ASAP.- !defCF = forceEither $ CF.readstring CF.emptyCP unConfig- !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: " ++ s ++ "." ++ o- else CP $ forceEither $ CF.set conf s o v---- | Gets a random generator from the config or,--- if not present, generates one and updates the config with it.-getSetGen :: CP -- ^ config- -> String -- ^ name of the generator- -> IO (R.StdGen, CP)-getSetGen config option =- case getOption config "engine" option of- Just sg -> return (read sg, config)- Nothing -> do- -- Pick the randomly chosen generator from the IO monad- -- and record it in the config for debugging (can be 'D'umped).- g <- R.newStdGen- let gs = show g- c = set config "engine" option gs- return (g, c)---- | The content of the configuration file. It's parsed--- in a case sensitive way (unlike by default in ConfigFile).-newtype CP = CP CF.ConfigParser--instance Show CP where- show (CP conf) = show $ CF.to_string conf---- | Switches all names to case sensitive (unlike by default in--- the "ConfigFile" library) and wraps in the constructor.-toCP :: CF.ConfigParser -> CP-toCP cf = CP $ cf {CF.optionxform = id}---- | In case of corruption, just fail.-forceEither :: Show a => Either a b -> b-forceEither (Left a) = assert `failure` a-forceEither (Right b) = b---- | A simplified access to an option in a given section,--- with simple error reporting (no internal errors are caught nor hidden).--- If there is no such option, gives Nothing.-getOption :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> Maybe a-getOption (CP conf) s o =- if CF.has_option conf s o- then Just $ forceEither $ CF.get conf s o- else Nothing---- | Simplified access to an option in a given section.--- Fails if the option is not present.-get :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> a-get (CP conf) s o =- if CF.has_option conf s o- then forceEither $ CF.get conf s o- else assert `failure` "Unknown config option: " ++ s ++ "." ++ o---- | An association list corresponding to a section. Fails if no such section.-getItems :: CP -> CF.SectionSpec -> [(String, String)]-getItems (CP conf) s =- if CF.has_section conf s- then forceEither $ CF.items conf s- else assert `failure` "Unknown config section: " ++ s--parseConfigRules :: CP -> Config-parseConfigRules cp =- let configSelfString = let CP conf = cp in CF.to_string conf- configCaves = map (\(n, t) -> (T.pack n, T.pack t)) $ getItems cp "caves"- configDepth = get cp "dungeon" "depth"- configFovMode = get cp "engine" "fovMode"- configSmellTimeout = get cp "engine" "smellTimeout"- configBaseHP = get cp "heroes" "baseHP"- configExtraHeroes = get cp "heroes" "extraHeroes"- configFirstDeathEnds = get cp "heroes" "firstDeathEnds"- configFaction = T.pack $ get cp "heroes" "faction"- in Config{..}--parseConfigUI :: FilePath -> CP -> ConfigUI-parseConfigUI dataDir cp =- let mkKey s =- case K.keyTranslate s of- K.Unknown _ ->- assert `failure` ("unknown config file key <" ++ s ++ ">")- key -> key- configCommands =- let mkCommand (key, def) = (mkKey key, def)- section = getItems cp "commands"- in map mkCommand section- configAppDataDir = dataDir- configDiaryFile = dataDir </> get cp "files" "diaryFile"- configSaveFile = dataDir </> get cp "files" "saveFile"- configBkpFile = dataDir </> get cp "files" "saveFile" <.> ".bkp"- configScoresFile = dataDir </> get cp "files" "scoresFile"- configRulesCfgFile = dataDir </> "config.rules"- configUICfgFile = dataDir </> "config.ui"- configHeroNames =- let toNumber (ident, name) =- case stripPrefix "HeroName_" ident of- Just n -> (read n, T.pack name)- Nothing -> assert `failure` ("wrong hero name id " ++ ident)- section = getItems cp "heroNames"- in map toNumber section- configMacros =- let trMacro (from, to) =- let !fromTr = mkKey from- !toTr = mkKey to- in if fromTr == toTr- then assert `failure` "degenerate alias: " ++ show toTr- else (fromTr, toTr)- section = getItems cp "macros"- in map trMacro section- configFont = get cp "ui" "font"- configHistoryMax = get cp "ui" "historyMax"- in ConfigUI{..}---- | Read and parse rules config file and supplement it with random seeds.-mkConfigRules :: Kind.Ops RuleKind -> IO (Config, R.StdGen, R.StdGen)-mkConfigRules corule = do- let cpRulesDefault = rcfgRulesDefault $ Kind.stdRuleset corule- appData <- appDataDir- cpRules <- mkConfig cpRulesDefault $ appData </> "config.rules.ini"- (dungeonGen, cp2) <- getSetGen cpRules "dungeonRandomGenerator"- (startingGen, cp3) <- getSetGen cp2 "startingRandomGenerator"- return (parseConfigRules cp3, dungeonGen, startingGen)---- | Read and parse UI config file.-mkConfigUI :: Kind.Ops RuleKind -> IO ConfigUI-mkConfigUI corule = do- let cpUIDefault = rcfgUIDefault $ Kind.stdRuleset corule- appData <- appDataDir- cpUI <- mkConfig cpUIDefault $ appData </> "config.ui.ini"- return $ parseConfigUI appData cpUI
− Game/LambdaHack/Action/Frontend.hs
@@ -1,33 +0,0 @@--- | Display game data on the screen and receive user input--- using one of the available raw frontends and derived operations.-module Game.LambdaHack.Action.Frontend- ( -- * Re-exported part of the raw frontend- FrontendSession, startup, frontendName, nextEvent- -- * Derived operations- , displayFrame, promptGetKey- ) where--import Game.LambdaHack.Action.Frontend.Chosen-import qualified Game.LambdaHack.Key as K (Key, Modifier)-import Game.LambdaHack.Animation (SingleFrame(..))---- | Push a frame or a single frame's worth of delay to the frame queue.-displayFrame :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()-displayFrame fs isRunning = display fs True isRunning---- TODO: move promptGetKey here and then change its type to--- promptGetKey :: FrontendSession--- -> [((K.Key, K.Modifier), a)]--- -> ((K.Key, K.Modifier) -> a) -- ^ handle unexpected key--- -> SingleFrame--- -> IO a--- Then see if it can be used instead of the dangerous, low level nextEvent.--- | Display a prompt, wait for any of the specified keys (for any key,--- if the list is empty). Repeat if an unexpected key received.-promptGetKey :: FrontendSession -> [(K.Key, K.Modifier)] -> SingleFrame- -> IO (K.Key, K.Modifier)-promptGetKey sess keys frame = do- km <- promptGetAnyKey sess frame- if null keys || km `elem` keys- then return km- else promptGetKey sess keys frame
− Game/LambdaHack/Action/Frontend/Chosen.hs
@@ -1,21 +0,0 @@--- | Re-export the operations of the chosen raw frontend--- (determined at compile time with cabal flags).-{-# LANGUAGE CPP #-}-module Game.LambdaHack.Action.Frontend.Chosen- ( -- * Re-exported raw frontend- FrontendSession, startup, frontendName, nextEvent, promptGetAnyKey, display- ) where---- Wrapper for selected Display frontend.--#ifdef GTK-import Game.LambdaHack.Action.Frontend.Gtk as D-#elif VTY-import Game.LambdaHack.Action.Frontend.Vty as D-#elif CURSES-import Game.LambdaHack.Action.Frontend.Curses as D-#elif STD-import Game.LambdaHack.Action.Frontend.Std as D-#else-import Game.LambdaHack.Action.Frontend.Gtk as D-#endif
− Game/LambdaHack/Action/Frontend/Curses.hs
@@ -1,158 +0,0 @@--- | Text frontend based on HSCurses.-module Game.LambdaHack.Action.Frontend.Curses- ( -- * Session data type for the frontend- FrontendSession- -- * The output and input operations- , display, nextEvent, promptGetAnyKey- -- * Frontend administration tools- , frontendName, startup- ) where--import qualified UI.HSCurses.Curses as C-import qualified UI.HSCurses.CursesHelper as C-import qualified Data.List as L-import qualified Data.Map as M-import Control.Monad-import Data.Char (ord, chr)-import qualified Data.Text as T--import Game.LambdaHack.Utils.Assert-import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..))-import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.Animation (SingleFrame(..))---- | Session data maintained by the frontend.-data FrontendSession = FrontendSession- { swin :: C.Window -- ^ the window to draw to- , sstyles :: M.Map Color.Attr C.CursesStyle- -- ^ map from fore/back colour pairs to defined curses styles- }---- | The name of the frontend.-frontendName :: String-frontendName = "curses"---- | Starts the main program loop using the frontend input and output.-startup :: String -> (FrontendSession -> IO ()) -> IO ()-startup _ k = do- C.start--- C.keypad C.stdScr False -- TODO: may help to fix xterm keypad on Ubuntu- void $ C.cursSet C.CursorInvisible- let s = [ (Color.Attr{fg, bg}, C.Style (toFColor fg) (toBColor bg))- | fg <- [minBound..maxBound],- -- No more color combinations possible: 16*4, 64 is max.- bg <- Color.legalBG ]- nr <- C.colorPairs- when (nr < L.length s) $- C.end >>- (assert `failure`- "Terminal has too few color pairs (" ++ show nr ++ "). Giving up.")- let (ks, vs) = unzip s- ws <- C.convertStyles vs- let styleMap = M.fromList (zip ks ws)- k (FrontendSession C.stdScr styleMap)- C.end---- | Output to the screen via the frontend.-display :: FrontendSession -- ^ frontend session data- -> Bool- -> Bool- -> Maybe SingleFrame -- ^ the screen frame to draw- -> IO ()-display _ _ _ Nothing = return ()-display FrontendSession{..} _ _ (Just SingleFrame{..}) = do- -- let defaultStyle = C.defaultCursesStyle- -- Terminals with white background require this:- let defaultStyle = sstyles M.! Color.defaultAttr- 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- sequence_ [ C.setStyle (M.findWithDefault defaultStyle acAttr sstyles)- >> C.mvWAddStr swin (y + 1) x [acChar]- | (y, line) <- nm, (x, Color.AttrChar{..}) <- line ]- C.refresh---- | Input key via the frontend.-nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)-nextEvent _sess _ = keyTranslate `fmap` C.getKey C.refresh---- | Display a prompt, wait for any key.-promptGetAnyKey :: FrontendSession -> SingleFrame- -> IO (K.Key, K.Modifier)-promptGetAnyKey sess frame = do- display sess True True $ Just frame- nextEvent sess Nothing--keyTranslate :: C.Key -> (K.Key, K.Modifier)-keyTranslate e =- case e of- C.KeyChar '\ESC' -> (K.Esc, K.NoModifier)- C.KeyExit -> (K.Esc, K.NoModifier)- C.KeyChar '\n' -> (K.Return, K.NoModifier)- C.KeyChar '\r' -> (K.Return, K.NoModifier)- C.KeyEnter -> (K.Return, K.NoModifier)- C.KeyChar ' ' -> (K.Space, K.NoModifier)- C.KeyChar '\t' -> (K.Tab, K.NoModifier)- C.KeyBTab -> (K.BackTab, K.NoModifier)- C.KeyUp -> (K.Up, K.NoModifier)- C.KeyDown -> (K.Down, K.NoModifier)- C.KeyLeft -> (K.Left, K.NoModifier)- C.KeySLeft -> (K.Left, K.NoModifier)- 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.KeyNPage -> (K.PgDn, K.NoModifier)- C.KeyBeg -> (K.Begin, K.NoModifier)- C.KeyB2 -> (K.Begin, K.NoModifier)- C.KeyClear -> (K.Begin, K.NoModifier)- -- No KP_ keys; see https://github.com/skogsbaer/hscurses/issues/10- -- TODO: try to get the Control modifier for keypad keys from the escape- -- gibberish and use Control-keypad for KP_ movement.- C.KeyChar c- -- This case needs to be considered after Tab, since, apparently,- -- on some terminals ^i == Tab and Tab is more important for us.- | 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,- -- 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 (show e), K.NoModifier)--toFColor :: Color.Color -> C.ForegroundColor-toFColor Color.Black = C.BlackF-toFColor Color.Red = C.DarkRedF-toFColor Color.Green = C.DarkGreenF-toFColor Color.Brown = C.BrownF-toFColor Color.Blue = C.DarkBlueF-toFColor Color.Magenta = C.PurpleF-toFColor Color.Cyan = C.DarkCyanF-toFColor Color.White = C.WhiteF-toFColor Color.BrBlack = C.GreyF-toFColor Color.BrRed = C.RedF-toFColor Color.BrGreen = C.GreenF-toFColor Color.BrYellow = C.YellowF-toFColor Color.BrBlue = C.BlueF-toFColor Color.BrMagenta = C.MagentaF-toFColor Color.BrCyan = C.CyanF-toFColor Color.BrWhite = C.BrightWhiteF--toBColor :: Color.Color -> C.BackgroundColor-toBColor Color.Black = C.BlackB-toBColor Color.Red = C.DarkRedB-toBColor Color.Green = C.DarkGreenB-toBColor Color.Brown = C.BrownB-toBColor Color.Blue = C.DarkBlueB-toBColor Color.Magenta = C.PurpleB-toBColor Color.Cyan = C.DarkCyanB-toBColor Color.White = C.WhiteB-toBColor _ = C.BlackB -- a limitation of curses
− Game/LambdaHack/Action/Frontend/Gtk.hs
@@ -1,463 +0,0 @@--- | Text frontend based on Gtk.-{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}-module Game.LambdaHack.Action.Frontend.Gtk- ( -- * Session data type for the frontend- FrontendSession- -- * The output and input operations- , display, nextEvent, promptGetAnyKey- -- * Frontend administration tools- , frontendName, startup- ) where--import Control.Monad-import Control.Monad.Reader-import Control.Concurrent-import Control.Exception (finally)-import Graphics.UI.Gtk.Gdk.EventM-import Graphics.UI.Gtk hiding (Point)-import qualified Data.List as L-import Data.IORef-import Data.Maybe-import qualified Data.Map as M-import qualified Data.ByteString.Char8 as BS-import System.Time-import qualified Data.Text as T-import Data.Text.Encoding (encodeUtf8)--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Utils.LQueue-import qualified Game.LambdaHack.Key as K (Key(..), keyTranslate, Modifier(..))-import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.Animation (SingleFrame(..))--data FrameState =- FPushed -- frames stored in a queue, to be drawn in equal time intervals- { fpushed :: !(LQueue (Maybe GtkFrame)) -- ^ screen output channel- , fshown :: !GtkFrame -- ^ last full frame shown- }- | FSet -- a single frame stored, to be drawn when a keypress is requested- { fsetFrame :: !(Maybe GtkFrame) -- ^ frame to draw at input key- }- | FNone -- no frames stored---- | Session data maintained by the frontend.-data FrontendSession = FrontendSession- { sview :: !TextView -- ^ the widget to draw to- , stags :: !(M.Map Color.Attr TextTag) -- ^ text color tags for fg/bg- , schanKey :: !(Chan (K.Key, K.Modifier)) -- ^ channel for keyboard input- , sframeState :: !(MVar FrameState) -- ^ state of the frame machine- , slastFull :: !(IORef (GtkFrame, Bool))- -- ^ most recent full (not empty, not repeated) frame received- -- and if any empty frame followed it- }--data GtkFrame = GtkFrame- { gfChar :: !BS.ByteString- , gfAttr :: ![[TextTag]]- }- deriving Eq--dummyFrame :: GtkFrame-dummyFrame = GtkFrame BS.empty []---- | Remove all but the last element of the frame queue.--- The kept last element ensures that slastFull is not invalidated.-trimQueue :: FrontendSession -> IO ()-trimQueue FrontendSession{sframeState} = do- fs <- takeMVar sframeState- case fs of- FPushed{..} ->- putMVar sframeState FPushed{fpushed = trimLQueue fpushed, ..}- _ ->- putMVar sframeState fs---- | The name of the frontend.-frontendName :: String-frontendName = "gtk"---- | Spawns the gtk input and output thread, which spawns all the other--- required threads. We create a separate thread for gtk to minimize--- communication with the heavy main thread. The other threads have to be--- spawned after gtk is initialized, because they call @postGUIAsync@,--- and need @sview@ and @stags@.-startup :: String -> (FrontendSession -> IO ()) -> IO ()-startup configFont k = do- mv <- newEmptyMVar- -- Fork the gtk input and output thread.- -- TODO: when GHC changes, make sure GTK is still faster on its own thread.- void $ forkIO (runGtk configFont k `finally` putMVar mv ())- takeMVar mv---- | Sets up and starts the main GTK loop providing input and output.-runGtk :: String -> (FrontendSession -> IO ()) -> IO ()-runGtk configFont k = do- -- Init GUI.- unsafeInitGUIForThreadedRTS- -- Text attributes.- ttt <- textTagTableNew- stags <- fmap M.fromList $- mapM (\ ak -> do- tt <- textTagNew Nothing- textTagTableAdd ttt tt- doAttr tt ak- return (ak, tt))- [ Color.Attr{fg, bg}- | fg <- [minBound..maxBound], bg <- Color.legalBG ]- -- Text buffer.- tb <- textBufferNew (Just ttt)- -- Create text view. TODO: use GtkLayout or DrawingArea instead of TextView?- sview <- textViewNewWithBuffer tb- textViewSetEditable sview False- textViewSetCursorVisible sview False- -- Set up the channel for keyboard input.- schanKey <- newChan- -- Set up the frame state.- let frameState = FNone- -- Create the session record.- sframeState <- newMVar frameState- slastFull <- newIORef (dummyFrame, False)- let sess = FrontendSession{..}- -- Fork the game logic thread. When logic ends, game exits.- -- TODO: is postGUIAsync needed here?- forkIO $ k sess >> postGUIAsync mainQuit- -- Fork the thread that periodically draws a frame from a queue, if any.- forkIO $ pollFrames sess Nothing- -- Fill the keyboard channel.- sview `on` keyPressEvent $ do- n <- eventKeyName- mods <- eventModifier- let !key = K.keyTranslate n- !modifier = modifierTranslate mods- liftIO $ do- unless (deadKey n) $ do- -- Drop all the old frames. Some more may be arriving at the same time.- trimQueue sess- -- Store the key in the channel.- writeChan schanKey (key, modifier)- return True- -- Set the font specified in config, if any.- f <- fontDescriptionFromString configFont- widgetModifyFont sview (Just f)- -- Prepare font chooser dialog.- currentfont <- newIORef f- sview `on` buttonPressEvent $ do- but <- eventButton- liftIO $ case but of- RightButton -> do- fsd <- fontSelectionDialogNew "Choose font"- cf <- readIORef currentfont -- TODO: "Terminus,Monospace" fails- fds <- fontDescriptionToString cf- fontSelectionDialogSetFontName fsd fds- fontSelectionDialogSetPreviewText fsd "eee...@.##+##"- resp <- dialogRun fsd- when (resp == ResponseOk) $ do- fn <- fontSelectionDialogGetFontName fsd- case fn of- Just fn' -> do- fd <- fontDescriptionFromString fn'- writeIORef currentfont fd- widgetModifyFont sview (Just fd)- Nothing -> return ()- widgetDestroy fsd- return True- _ -> return False- -- Modify default colours.- let black = Color minBound minBound minBound -- Color.defBG == Color.Black- white = Color 0xC500 0xBC00 0xB800 -- Color.defFG == Color.White- widgetModifyBase sview StateNormal black- widgetModifyText sview StateNormal white- -- Set up the main window.- w <- windowNew- containerAdd w sview- onDestroy w mainQuit- widgetShowAll w- -- Wait until the other thread draws something and show the window.- yield- mainGUI---- | Output to the screen via the frontend.-output :: FrontendSession -- ^ frontend session data- -> GtkFrame -- ^ the screen frame to draw- -> IO ()-output FrontendSession{sview, stags} GtkFrame{..} = do -- new frame- tb <- textViewGetBuffer sview- let attrs = L.zip [0..] gfAttr- defaultAttr = stags M.! Color.defaultAttr- textBufferSetByteString tb gfChar- mapM_ (setTo tb defaultAttr 0) attrs--setTo :: TextBuffer -> TextTag -> Int -> (Int, [TextTag]) -> IO ()-setTo _ _ _ (_, []) = return ()-setTo tb defaultAttr lx (ly, attr:attrs) = do- ib <- textBufferGetIterAtLineOffset tb (ly + 1) lx- ie <- textIterCopy ib- let setIter :: TextTag -> Int -> [TextTag] -> IO ()- setIter previous repetitions [] = do- textIterForwardChars ie repetitions- when (previous /= defaultAttr) $- textBufferApplyTag tb previous ib ie- setIter previous repetitions (a:as)- | a == previous =- setIter a (repetitions + 1) as- | otherwise = do- textIterForwardChars ie repetitions- when (previous /= defaultAttr) $- textBufferApplyTag tb previous ib ie- textIterForwardChars ib repetitions- setIter a 1 as- setIter attr 1 attrs---- TODO: configure--- | Maximal frames per second.--- This is better low and fixed, to avoid jerkiness and delays--- that tell the player there are many intelligent enemies on the level.--- That's better than scaling AI sofistication down based on the FPS setting--- and machine speed.-maxFps :: Int-maxFps = 15---- | Maximal polls per second.-maxPolls :: Int-maxPolls = let maxP = 120- in assert (maxP >= 2 * maxFps `blame` (maxP, maxFps)) $- maxP---- | Add a given number of microseconds to time.-addTime :: ClockTime -> Int -> ClockTime-addTime (TOD s p) ms = TOD s (p + fromIntegral (ms * 1000000))---- | The difference between the first and the second time, in microseconds.-diffTime :: ClockTime -> ClockTime -> Int-diffTime (TOD s1 p1) (TOD s2 p2) =- (fromIntegral $ s1 - s2) * 1000000 +- (fromIntegral $ p1 - p2) `div` 1000000---- | Poll the frame queue often and draw frames at fixed intervals.-pollFrames :: FrontendSession -> Maybe ClockTime -> IO ()-pollFrames sess (Just setTime) = do- -- Check if the time is up.- curTime <- getClockTime- let diffT = diffTime setTime curTime- if diffT > 1000000 `div` maxPolls- then do- -- Delay half of the time difference.- threadDelay $ diffTime curTime setTime `div` 2- pollFrames sess $ Just setTime- else- -- Don't delay, because time is up!- pollFrames sess Nothing-pollFrames sess@FrontendSession{sframeState} Nothing = do- -- Time time is up, check if we actually wait for anyting.- fs <- takeMVar sframeState- case fs of- FPushed{..} ->- case tryReadLQueue fpushed of- Just (Just frame, queue) -> do- -- The frame has arrived so send it for drawing and update delay.- putMVar sframeState FPushed{fpushed = queue, fshown = frame}- postGUIAsync $ output sess frame- curTime <- getClockTime- threadDelay $ 1000000 `div` (maxFps * 2)- pollFrames sess $ Just $ addTime curTime $ 1000000 `div` maxFps- Just (Nothing, queue) -> do- -- Delay requested via an empty frame.- putMVar sframeState FPushed{fpushed = queue, ..}- curTime <- getClockTime- -- There is no problem if the delay is a bit delayed.- threadDelay $ 1000000 `div` maxFps- pollFrames sess $ Just $ addTime curTime $ 1000000 `div` maxFps- Nothing -> do- -- The queue is empty, the game logic thread lags.- putMVar sframeState fs- -- Time time is up, the game thread is going to send a frame,- -- (otherwise it would change the state), so poll often.- threadDelay $ 1000000 `div` maxPolls- pollFrames sess Nothing- _ -> do- putMVar sframeState fs- -- Not in the Push state, so poll lazily to catch the next state change.- -- The slow polling also gives the game logic a head start- -- in creating frames in case one of the further frames is slow- -- to generate and would normally cause a jerky delay in drawing.- threadDelay $ 1000000 `div` (maxFps * 2)- pollFrames sess Nothing---- | Add a frame to be drawn.-display :: FrontendSession -> Bool -> Bool -> Maybe SingleFrame -> IO ()-display sess True noDelay rawFrame = pushFrame sess noDelay rawFrame-display sess False _ (Just rawFrame) = setFrame sess rawFrame-display _ _ _ _ = assert `failure` "display: empty frame to be set"---- | Add a game screen frame to the frame drawing channel.-pushFrame :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()-pushFrame sess@FrontendSession{sframeState, slastFull} noDelay rawFrame = do- -- Full evaluation and comparison is done outside the mvar lock.- (lastFrame, anyFollowed) <- readIORef slastFull- let frame = maybe Nothing (Just . evalFrame sess) rawFrame- nextFrame =- if frame == Just lastFrame- then Nothing -- no sense repeating- else frame- -- Now we take the lock.- fs <- takeMVar sframeState- case fs of- FPushed{..} ->- if (isNothing nextFrame && anyFollowed)- then putMVar sframeState fs -- old news- else putMVar sframeState- FPushed{fpushed = writeLQueue fpushed nextFrame, ..}- FSet{} -> assert `failure` "pushFrame: FSet, expecting FPushed or FNone"- FNone ->- -- Never start playing with an empty frame.- let fpushed = if isJust nextFrame- then writeLQueue newLQueue nextFrame- else newLQueue- fshown = dummyFrame- in putMVar sframeState FPushed{..}- yield -- drawing has priority- case nextFrame of- Nothing -> writeIORef slastFull (lastFrame, True)- Just f -> writeIORef slastFull (f, noDelay)--evalFrame :: FrontendSession -> SingleFrame -> GtkFrame-evalFrame FrontendSession{stags} SingleFrame{..} =- let levelChar = L.map (T.pack . L.map Color.acChar) sfLevel- gfChar = encodeUtf8 $ T.intercalate (T.singleton '\n')- $ sfTop : levelChar ++ [sfBottom]- -- Strict version of @L.map (L.map ((stags M.!) . fst)) sfLevel@.- gfAttr = L.reverse $ L.foldl' ff [] sfLevel- ff ll l = (L.reverse $ L.foldl' f [] l) : ll- f l ac = let !tag = stags M.! Color.acAttr ac in tag : l- in GtkFrame{..}---- | Set the frame to be drawn at the next invocation of @nextEvent@.--- Fail if there is already a frame pushed or set.--- Don't show the frame if it's unchanged vs the previous.-setFrame :: FrontendSession -> SingleFrame -> IO ()-setFrame sess@FrontendSession{slastFull, sframeState} rawFrame = do- -- Full evaluation and comparison is done outside the mvar lock.- (lastFrame, _) <- readIORef slastFull- let frame = evalFrame sess rawFrame- fsetFrame =- if frame == lastFrame- then Nothing -- no sense repeating- else Just frame- -- Now we take the lock.- fs <- takeMVar sframeState- case fs of- FPushed{} -> assert `failure` "setFrame: FPushed, expecting FNone"- FSet{} -> assert `failure` "setFrame: FSet, expecting FNone"- FNone -> do- -- Update the last received frame with the processed frame.- -- There is no race condition, because we are on the same thread- -- as pushFrame.- maybe (return ()) (\ fr -> writeIORef slastFull (fr, False)) fsetFrame- -- Store the frame. Release the lock.- putMVar sframeState FSet{..}---- | Input key via the frontend. Fail if there is no frame to show--- to the player as a prompt for the keypress.-nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)-nextEvent FrontendSession{schanKey, sframeState} Nothing = do- -- Verify the state.- -- Assumption: no other thread changes the main constructor in sframeState.- fs <- readMVar sframeState- case fs of- FNone -> return () -- old frame requested, as expected- FPushed{} -> assert `failure` "nextEvent: FPushed, expecting FNone"- FSet{} -> assert `failure` "nextEvent: FSet, expecting FNone"- -- Wait for a keypress.- km <- readChan schanKey- return km-nextEvent sess@FrontendSession{schanKey, sframeState} (Just False) = do- -- Take the lock to display the set frame.- fs <- takeMVar sframeState- case fs of- FSet{fsetFrame} -> do- -- If the frame not repeated, draw it.- maybe (return ()) (postGUIAsync . output sess) fsetFrame- FPushed{} -> assert `failure` "nextEvent: FPushed, expecting FSet"- FNone -> assert `failure` "nextEvent: FNone, expecting FSet"- -- Clear the stored frame. Release the lock.- putMVar sframeState FNone- -- Wait for a keypress.- km <- readChan schanKey- return km-nextEvent sess@FrontendSession{schanKey, sframeState} (Just True) = do- -- Wait for a keypress.- km <- readChan schanKey- -- Trim the queue.- trimQueue sess- -- Take the lock to wipe out the frame queue, unless it's empty already.- fs <- takeMVar sframeState- case fs of- FPushed{..} -> do- -- Draw the last frame ASAP.- case tryReadLQueue fpushed of- Just (Just frame, queue) -> assert (nullLQueue queue) $ do- -- Comparison is done inside the mvar lock, this time, but it's OK.- let lastFrame = fshown- nextFrame =- if frame == lastFrame- then Nothing -- no sense repeating- else Just frame- maybe (return ()) (postGUIAsync . output sess) nextFrame- Just (Nothing, _) -> assert `failure` "nextEvent: trimmed queue"- Nothing -> return ()- FSet{} -> assert `failure` "nextEvent: FSet, expecting FPushed"- FNone -> assert `failure` "nextEvent: FNone, expecting FPushed"- -- Wipe out the frame queue. No more frames will arrive, because we are- -- on the same thread as pushFrame. Release the lock.- putMVar sframeState FNone- return km---- | Display a prompt, wait for any key.--- Starts in Push or None mode, stop in None mode.--- Spends most time waiting for a key, so not performance critical,--- so does not need optimization.-promptGetAnyKey :: FrontendSession -> SingleFrame- -> IO (K.Key, K.Modifier)-promptGetAnyKey sess@FrontendSession{sframeState} frame = do- -- Assumption: no other thread changes the main constructor in sframeState.- fs <- readMVar sframeState- yield -- drawing has priority- let doPush = case fs of- FPushed{} -> True- FSet{} ->- assert `failure` "promptGetKey: FSet, expecting FPushed or FNone"- FNone -> False- display sess doPush True $ Just frame- nextEvent sess (Just doPush)---- | Tells a dead key.-deadKey :: String -> Bool-deadKey x = case x of- "Shift_R" -> True- "Shift_L" -> True- "Control_L" -> True- "Control_R" -> True- "Super_L" -> True- "Super_R" -> True- "Menu" -> True- "Alt_L" -> True- "Alt_R" -> True- "ISO_Level2_Shift" -> True- "ISO_Level3_Shift" -> True- "ISO_Level2_Latch" -> True- "ISO_Level3_Latch" -> True- "Num_Lock" -> True- "Caps_Lock" -> True- _ -> False---- | Translates modifiers to our own encoding.-modifierTranslate :: [Modifier] -> K.Modifier-modifierTranslate mods =- if Control `elem` mods then K.Control else K.NoModifier--doAttr :: TextTag -> Color.Attr -> IO ()-doAttr tt attr@Color.Attr{fg, bg}- | attr == Color.defaultAttr = return ()- | fg == Color.defFG = set tt [textTagBackground := Color.colorToRGB bg]- | bg == Color.defBG = set tt [textTagForeground := Color.colorToRGB fg]- | otherwise = set tt [textTagForeground := Color.colorToRGB fg,- textTagBackground := Color.colorToRGB bg]
− Game/LambdaHack/Action/Frontend/Std.hs
@@ -1,107 +0,0 @@--- | Text frontend based on stdin/stdout, intended for bots.-module Game.LambdaHack.Action.Frontend.Std- ( -- * Session data type for the frontend- FrontendSession- -- * The output and input operations- , display, nextEvent, promptGetAnyKey- -- * Frontend administration tools- , frontendName, startup- ) where--import qualified Data.List as L-import qualified Data.ByteString.Char8 as BS-import qualified System.IO as SIO-import Data.Text.Encoding (encodeUtf8)--import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..))-import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.Animation (SingleFrame(..))---- | No session data needs to be maintained by this frontend.-type FrontendSession = ()---- | The name of the frontend.-frontendName :: String-frontendName = "std"---- | Starts the main program loop using the frontend input and output.-startup :: String -> (FrontendSession -> IO ()) -> IO ()-startup _ k = k ()---- | Output to the screen via the frontend.-display :: FrontendSession -- ^ frontend session data- -> Bool- -> Bool- -> Maybe SingleFrame -- ^ the screen frame to draw- -> IO ()-display _ _ _ Nothing = return ()-display _ _ _ (Just SingleFrame{..}) =- let chars = L.map (BS.pack . L.map Color.acChar) sfLevel- bs = [encodeUtf8 sfTop, BS.empty] ++ chars ++ [encodeUtf8 sfBottom, BS.empty]- in mapM_ BS.putStrLn bs---- | Input key via the frontend.-nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)-nextEvent sess mb = do- e <- BS.hGet SIO.stdin 1- let c = BS.head e- if c == '\n' -- let \n mark the end of input, for human players- then nextEvent sess mb- else return (keyTranslate c, K.NoModifier)---- | Display a prompt, wait for any key.-promptGetAnyKey :: FrontendSession -> SingleFrame- -> IO (K.Key, K.Modifier)-promptGetAnyKey sess frame = do- display sess True True $ Just frame- nextEvent sess Nothing---- HACK: Special translation that block commands the bots should not use--- and multiplies some other commands.-keyTranslate :: Char -> K.Key-keyTranslate e =- case e of- -- Translate some special keys (use vi keys to move).- '\ESC' -> K.Esc- '\n' -> K.Return- '\r' -> K.Return- '\t' -> K.Tab- -- For bots: disable purely UI commands.- 'P' -> K.Char 'U'- 'V' -> K.Char 'Y'- 'O' -> K.Char 'J'- 'I' -> K.Char 'L'- 'R' -> K.Char 'K'- '?' -> K.Char 'N'- -- For bots: don't let them give up, write files, procrastinate.- 'Q' -> K.Char 'H'- 'X' -> K.Char 'B'- 'D' -> K.Return- '.' -> K.Return- -- For bots (assuming they go from '0' to 'z'): major commands.- '<' -> K.Char 'q' -- ban ascending to speed up descending- '>' -> K.Char '>'- 'c' -> K.Char 'c'- 'd' -> K.Char 'r' -- don't let bots drop stuff- 'g' -> K.Char 'g'- 'i' -> K.Char 'i'- 'o' -> K.Char 'o'- 'q' -> K.Char 'q'- 'r' -> K.Char 'r'- 't' -> K.Char 'g' -- tagetting is too hard, so don't throw- 'z' -> K.Char 'g' -- and don't zap- 'p' -> K.Char 'g' -- and don't project- 'a' -> K.Esc -- and don't apply- -- For bots: minor commands. Targeting is too hard, so don't do it.- '*' -> K.Char 'c'- '/' -> K.Char 'c'- '[' -> K.Char 'g'- ']' -> K.Char 'g'- '{' -> K.Char 'g'- '}' -> K.Char 'g'- -- Hack for bots: dump config at the start.- ' ' -> K.Char 'D'- -- Movement and hero selection.- c | c `elem` "kjhlyubnKJHLYUBN" -> K.Char c- c | c `elem` ['0'..'9'] -> K.Char c- _ -> K.Char '>' -- try hard to descend
− Game/LambdaHack/Action/Frontend/Vty.hs
@@ -1,128 +0,0 @@--- | Text frontend based on Vty.-module Game.LambdaHack.Action.Frontend.Vty- ( -- * Session data type for the frontend- FrontendSession- -- * The output and input operations- , display, nextEvent, promptGetAnyKey- -- * Frontend administration tools- , frontendName, startup- ) where--import Graphics.Vty-import qualified Graphics.Vty as Vty-import qualified Data.List as L-import Data.Text.Encoding (encodeUtf8)--import qualified Game.LambdaHack.Key as K (Key(..), Modifier(..))-import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.Animation (SingleFrame(..))---- | Session data maintained by the frontend.-type FrontendSession = Vty---- | The name of the frontend.-frontendName :: String-frontendName = "vty"---- | Starts the main program loop using the frontend input and output.-startup :: String -> (FrontendSession -> IO ()) -> IO ()-startup _ k = do- fs <- mkVty- k fs- Vty.shutdown fs---- | Output to the screen via the frontend.-display :: FrontendSession -- ^ frontend session data- -> Bool- -> Bool- -> Maybe SingleFrame -- ^ the screen frame to draw- -> IO ()-display _ _ _ Nothing = return ()-display vty _ _ (Just SingleFrame{..}) =- let img = (foldr (<->) empty_image- . L.map (foldr (<|>) empty_image- . L.map (\ Color.AttrChar{..} ->- char (setAttr acAttr) acChar)))- sfLevel- pic = pic_for_image $- utf8_bytestring (setAttr Color.defaultAttr) (encodeUtf8 sfTop)- <-> img <->- utf8_bytestring (setAttr Color.defaultAttr) (encodeUtf8 sfBottom)- in update vty pic---- | Input key via the frontend.-nextEvent :: FrontendSession -> Maybe Bool -> IO (K.Key, K.Modifier)-nextEvent sess mb = do- e <- next_event sess- case e of- EvKey n mods -> do- let key = keyTranslate n- modifier = modifierTranslate mods- return (key, modifier)- _ -> nextEvent sess mb---- | Display a prompt, wait for any key.-promptGetAnyKey :: FrontendSession -> SingleFrame- -> IO (K.Key, K.Modifier)-promptGetAnyKey sess frame = do- display sess True True $ Just frame- nextEvent sess Nothing--keyTranslate :: Key -> K.Key-keyTranslate n =- case n of- KEsc -> K.Esc- KEnter -> K.Return- (KASCII ' ') -> K.Space- (KASCII '\t') -> K.Tab- KBackTab -> K.BackTab- KUp -> K.Up- KDown -> K.Down- KLeft -> K.Left- KRight -> K.Right- KHome -> K.Home- KPageUp -> K.PgUp- KEnd -> K.End- KPageDown -> K.PgDn- KBegin -> K.Begin- (KASCII c) -> K.Char c- _ -> K.Unknown (show n)---- | Translates modifiers to our own encoding.-modifierTranslate :: [Modifier] -> K.Modifier-modifierTranslate mods =- if MCtrl `elem` mods then K.Control else K.NoModifier---- 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.-hack :: Color.Color -> Attr -> Attr-hack c a = if Color.isBright c then with_style a bold else a--setAttr :: Color.Attr -> Attr-setAttr Color.Attr{fg, bg} =--- This optimization breaks display for white background terminals:--- if (fg, bg) == Color.defaultAttr--- then def_attr--- else- hack fg $ hack bg $- def_attr { attr_fore_color = SetTo (aToc fg)- , attr_back_color = SetTo (aToc bg) }--aToc :: Color.Color -> Color-aToc Color.Black = black-aToc Color.Red = red-aToc Color.Green = green-aToc Color.Brown = yellow-aToc Color.Blue = blue-aToc Color.Magenta = magenta-aToc Color.Cyan = cyan-aToc Color.White = white-aToc Color.BrBlack = bright_black-aToc Color.BrRed = bright_red-aToc Color.BrGreen = bright_green-aToc Color.BrYellow = bright_yellow-aToc Color.BrBlue = bright_blue-aToc Color.BrMagenta = bright_magenta-aToc Color.BrCyan = bright_cyan-aToc Color.BrWhite = bright_white
− Game/LambdaHack/Action/HighScore.hs
@@ -1,152 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | High score table operations.-module Game.LambdaHack.Action.HighScore- ( register- ) where--import System.Directory-import Control.Monad-import Text.Printf-import System.Time-import Data.Binary-import qualified Data.List as L-import Data.Text (Text)-import qualified Data.Text as T-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.File-import Game.LambdaHack.Config-import Game.LambdaHack.Dungeon-import Game.LambdaHack.Misc-import Game.LambdaHack.Time-import Game.LambdaHack.Msg-import Game.LambdaHack.State---- | 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- }- deriving (Eq, Ord)--instance Binary ScoreRecord where- put (ScoreRecord p n (TOD cs cp) s) = do- put p- put n- put cs- put cp- put s- get = do- p <- get- n <- get- cs <- get- cp <- get- s <- get- return (ScoreRecord p n (TOD cs cp) s)---- | Show a single high score, from the given ranking in the high score table.-showScore :: (Int, ScoreRecord) -> [Text]-showScore (pos, score) =- let died = case status score of- Killed lvl -> "perished on level " ++ show (levelNumber lvl) ++ ","- Camping -> "is camping somewhere,"- Victor -> "emerged victorious"- Restart -> "resigned prematurely"- curDate = calendarTimeToString . toUTCTime . date $ score- big, lil :: String- big = " "- lil = " "- turns = - (negTime score `timeFit` timeTurn)- -- TODO: the spaces at the end are hand-crafted. Remove when display- -- of overlays adds such spaces automatically.- in map T.pack- [ big- , printf "%4d. %6d This adventuring party %s after %d turns "- pos (points score) died turns- , lil ++ printf "on %s. " curDate- ]---- | The list of scores, in decreasing order.-type ScoreTable = [ScoreRecord]---- | Empty score table-empty :: ScoreTable-empty = []---- | Save a simple serialized version of the high scores table.-save :: ConfigUI -> ScoreTable -> IO ()-save ConfigUI{configScoresFile} scores = encodeEOF configScoresFile scores---- | Read the high scores table. Return the empty table if no file.-restore :: ConfigUI -> IO ScoreTable-restore ConfigUI{configScoresFile} = do- b <- doesFileExist configScoresFile- if not b- then return empty- else strictDecodeEOF configScoresFile---- | Insert a new score into the table, Return new table and the ranking.-insertPos :: ScoreRecord -> ScoreTable -> (ScoreTable, Int)-insertPos s h =- let (prefix, suffix) = L.span (> s) h- in (prefix ++ [s] ++ suffix, L.length prefix + 1)---- | 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 h start height =- let zipped = zip [1..] h- screenful = take height . drop (start - 1) $ zipped- in concatMap showScore screenful---- | Produce a couple of renderings of the high scores table.-slideshow :: Int -> ScoreTable -> Int -> [Overlay]-slideshow 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]---- | Take care of saving a new score to the table--- and return a list of messages to display.-register :: ConfigUI -- ^ the config file- -> Bool -- ^ whether to write or only render- -> Int -- ^ the total score. not halved yet- -> Time -- ^ game time spent- -> ClockTime -- ^ date of the last game interruption- -> Status -- ^ reason of the game interruption- -> IO (Msg, [Overlay])-register configUI write total time date status = do- h <- restore configUI- let points = case status of- Killed _ -> (total + 1) `div` 2- _ -> total- negTime = timeNegate time- score = ScoreRecord{..}- (h', pos) = insertPos score h- (_, nlines) = normalLevelBound -- TODO: query terminal size instead- height = nlines `div` 3- (subject, person, msgUnless) =- case status of- Killed lvl | levelNumber lvl <= 1 ->- ("your short-lived struggle", MU.Sg3rd, "(score halved)")- Killed _ ->- ("your heroic deeds", MU.PlEtc, "(score halved)")- Camping ->- ("your valiant exploits", MU.PlEtc, "(unless you are slain)")- Victor ->- ("your glorious victory", MU.Sg3rd,- if pos <= height- then "among the greatest heroes"- else "")- Restart ->- ("your abortive attempt", MU.Sg3rd, "(score halved)")- msg = makeSentence- [ MU.SubjectVerb person MU.Yes subject "award you"- , MU.Ordinal pos, "place"- , msgUnless ]- when write $ save configUI h'- return (msg, slideshow pos h' height)
− Game/LambdaHack/Action/Save.hs
@@ -1,143 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Saving and restoring games and player diaries.-module Game.LambdaHack.Action.Save- ( saveGameFile, restoreGame, rmBkpSaveDiary, saveGameBkp- ) where--import System.Directory-import System.FilePath-import qualified Control.Exception as Ex hiding (handle)-import Control.Monad-import Control.Concurrent-import System.IO.Unsafe (unsafePerformIO) -- horrors-import Data.Text (Text)-import qualified Data.Text as T--import Game.LambdaHack.Utils.File-import Game.LambdaHack.State-import Game.LambdaHack.Msg-import Game.LambdaHack.Config---- | Save a simple serialized version of the current player diary.-saveDiary :: FilePath -> Diary -> IO ()-saveDiary configDiaryFile diary = encodeEOF configDiaryFile diary--saveLock :: MVar ()-{-# NOINLINE saveLock #-}-saveLock = unsafePerformIO newEmptyMVar---- | Save a simple serialized version of the current state.--- Protected by a lock to avoid corrupting the file.-saveGameFile :: ConfigUI -> State -> IO ()-saveGameFile ConfigUI{configSaveFile} state = do- putMVar saveLock ()- encodeEOF configSaveFile state- takeMVar saveLock---- | Try to create a directory. Hide errors due to,--- e.g., insufficient permissions, because the game can run--- in the current directory just as well.-tryCreateDir :: FilePath -> IO ()-tryCreateDir dir =- Ex.catch- (createDirectory dir)- (\ e -> case e :: Ex.IOException of _ -> return ())---- | Try to copy over data files. Hide errors due to,--- e.g., insufficient permissions, because the game can run--- without data files just as well.-tryCopyDataFiles :: ConfigUI -> (FilePath -> IO FilePath) -> IO ()-tryCopyDataFiles ConfigUI{ configScoresFile- , configRulesCfgFile- , configUICfgFile } pathsDataFile = do- rulesFile <- pathsDataFile $ takeFileName configRulesCfgFile <.> ".default"- uiFile <- pathsDataFile $ takeFileName configUICfgFile <.> ".default"- scoresFile <- pathsDataFile $ takeFileName configScoresFile- let newRulesFile = configRulesCfgFile <.> ".ini"- newUIFile = configUICfgFile <.> ".ini"- newScoresFile = configScoresFile- Ex.catch- (copyFile rulesFile newRulesFile >>- copyFile uiFile newUIFile >>- copyFile scoresFile newScoresFile)- (\ e -> case e :: Ex.IOException of _ -> return ())---- | Restore a saved game, if it exists. Initialize directory structure,--- if needed.-restoreGame :: ConfigUI -> (FilePath -> IO FilePath) -> Text- -> IO (Either (State, Diary, Msg) (Diary, Msg))-restoreGame config@ConfigUI{ configAppDataDir- , configDiaryFile- , configSaveFile- , configBkpFile } pathsDataFile title = do- ab <- doesDirectoryExist configAppDataDir- -- If the directory can't be created, the current directory will be used.- unless ab $ do- tryCreateDir configAppDataDir- -- Possibly copy over data files. No problem if it fails.- tryCopyDataFiles config pathsDataFile- -- If the diary file does not exist, create an empty diary.- -- TODO: when diary gets corrupted, start a new one, too.- diary <-- do db <- doesFileExist configDiaryFile- if db- then strictDecodeEOF configDiaryFile- else defaultDiary- -- If the savefile exists but we get IO errors, we show them,- -- back up the savefile and move it out of the way and start a new game.- -- If the savefile was randomly corrupted or made read-only,- -- that should solve the problem. If the problems are more serious,- -- the other functions will most probably also throw exceptions,- -- this time without trying to fix it up.- sb <- doesFileExist configSaveFile- bb <- doesFileExist configBkpFile- Ex.catch- (if sb- then do- renameFile configSaveFile configBkpFile- state <- strictDecodeEOF configBkpFile- let msg = "Welcome back to" <+> title <> "."- return $ Left (state, diary, msg)- else- if bb- then do- state <- strictDecodeEOF configBkpFile- let msg = "No savefile found. Restoring from a backup savefile."- return $ Left (state, diary, msg)- else return $ Right (diary, "Welcome to" <+> title <> "!"))- (\ e -> case e :: Ex.SomeException of- _ -> let msg = "Starting a new game, because restore failed."- <+> "The error message was:"- <+> (T.unwords . T.lines) (showT e)- in return $ Right (diary, msg))---- | Save the diary and a backup of the save game file, in case of crashes.--- This is only a backup, so no problem is the game is shut down--- before saving finishes, so we don't wait on the mvar. However,--- if a previous save is already in progress, we skip this save.-saveGameBkp :: ConfigUI -> State -> Diary -> IO ()-saveGameBkp ConfigUI{ configDiaryFile- , configSaveFile- , configBkpFile } state diary = do- b <- tryPutMVar saveLock ()- when b $- void $ forkIO $ do- saveDiary configDiaryFile diary -- save diary often in case of crashes- encodeEOF configSaveFile state- renameFile configSaveFile configBkpFile- takeMVar saveLock---- | Remove the backup of the savegame and save the player diary.--- Should be called before any non-error exit from the game.--- Sometimes the backup file does not exist and it's OK.--- We don't bother reporting any other removal exceptions, either,--- because the backup file is relatively unimportant.--- We wait on the mvar, because saving the diary at game shutdown is important.-rmBkpSaveDiary :: ConfigUI -> Diary -> IO ()-rmBkpSaveDiary ConfigUI{ configDiaryFile- , configBkpFile } diary = do- putMVar saveLock ()- saveDiary configDiaryFile diary -- save diary often in case of crashes- bb <- doesFileExist configBkpFile- when bb $ removeFile configBkpFile- takeMVar saveLock
− Game/LambdaHack/Actions.hs
@@ -1,644 +0,0 @@-{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}-{-# OPTIONS_GHC -fno-warn-type-defaults #-}--- | The game action stuff that is independent from ItemAction.hs--- (both depend on EffectAction.hs).--- TODO: Add an export list and document after it's rewritten according to #17.-module Game.LambdaHack.Actions where---- Cabal-import qualified Paths_LambdaHack as Self (version)--import Control.Monad-import Control.Monad.State hiding (State, state)-import qualified Data.List as L-import qualified Data.IntMap as IM-import qualified Data.Map as M-import Data.Maybe-import Data.Version-import qualified Data.IntSet as IS-import Data.Ratio-import Data.Text (Text)-import qualified Data.Text as T-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Action-import Game.LambdaHack.Point-import Game.LambdaHack.Vector-import qualified Game.LambdaHack.Dungeon as Dungeon-import Game.LambdaHack.Item-import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Level-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import Game.LambdaHack.Perception-import Game.LambdaHack.State-import qualified Game.LambdaHack.Effect as Effect-import Game.LambdaHack.EffectAction-import qualified Game.LambdaHack.Tile as Tile-import qualified Game.LambdaHack.Kind as Kind-import qualified Game.LambdaHack.Feature as F-import Game.LambdaHack.DungeonState-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Content.TileKind as TileKind-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Content.RuleKind-import Game.LambdaHack.Random-import Game.LambdaHack.Misc-import Game.LambdaHack.Msg-import Game.LambdaHack.Binding-import Game.LambdaHack.Time-import Game.LambdaHack.Animation (swapPlaces, blockMiss)-import Game.LambdaHack.Draw-import qualified Game.LambdaHack.Command as Command-import Game.LambdaHack.Config--default (Text)--gameSave :: Action ()-gameSave = do- saveGameBkp- msgAdd "Game progress saved to a backup file."--gameExit :: Action ()-gameExit = do- b <- displayYesNo "Really save and exit?"- if b- then modify (\ s -> s {squit = Just (True, Camping)})- else abortWith "Game resumed."--gameRestart :: Action ()-gameRestart = do- b1 <- displayMore ColorFull "You just requested a new game."- when (not b1) $ neverMind True- b2 <- displayYesNo "Current progress will be lost! Really restart the game?"- when (not b2) $ abortWith "Yea, so much still to do."- modify (\ s -> s {squit = Just (False, Restart)})--moveCursor :: Vector -> Int -> ActionFrame ()-moveCursor dir n = do- lxsize <- gets (lxsize . slevel)- lysize <- gets (lysize . slevel)- let upd cursor =- let shiftB loc =- shiftBounded lxsize (1, 1, lxsize - 2, lysize - 2) loc dir- cloc = iterate shiftB (clocation cursor) !! n- in cursor { clocation = cloc }- modify (updateCursor upd)- doLook---- TODO: Think about doing the mode dispatch elsewhere, especially if over--- time more and more commands need to do the dispatch inside their code--- (currently only a couple do).-move :: Vector -> ActionFrame ()-move dir = do- pl <- gets splayer- targeting <- gets (ctargeting . scursor)- if targeting /= TgtOff- then do- frs <- moveCursor dir 1- -- Mark that unexpectedly it does not take time.- modify (\ s -> s {stakeTime = Just False})- return frs- else- inFrame $ moveOrAttack True pl dir--ifRunning :: ((Vector, Int) -> Action a) -> Action a -> Action a-ifRunning t e = do- ad <- gets (bdir . getPlayerBody)- maybe e t ad---- | Guess and report why the bump command failed.-guessBump :: Kind.Ops TileKind -> F.Feature -> Kind.Id TileKind -> Action ()-guessBump cotile F.Openable t | Tile.hasFeature cotile F.Closable t =- abortWith "already open"-guessBump _ F.Openable _ =- abortWith "not a door"-guessBump cotile F.Closable t | Tile.hasFeature cotile F.Openable t =- abortWith "already closed"-guessBump _ F.Closable _ =- abortWith "not a door"-guessBump cotile F.Ascendable t | Tile.hasFeature cotile F.Descendable t =- abortWith "the way goes down, not up"-guessBump _ F.Ascendable _ =- abortWith "no stairs up"-guessBump cotile F.Descendable t | Tile.hasFeature cotile F.Ascendable t =- abortWith "the way goes up, not down"-guessBump _ F.Descendable _ =- abortWith "no stairs down"-guessBump _ _ _ = neverMind True---- | Player tries to trigger a tile using a feature.-bumpTile :: Point -> F.Feature -> Action ()-bumpTile dloc feat = do- Kind.COps{cotile} <- getCOps- lvl <- gets slevel- let t = lvl `at` dloc- if Tile.hasFeature cotile feat t- then triggerTile dloc- else guessBump cotile feat t---- | Perform the action specified for the tile in case it's triggered.-triggerTile :: Point -> Action ()-triggerTile dloc = do- Kind.COps{cotile=Kind.Ops{okind, opick}} <- getCOps- lvl <- gets slevel- let f (F.Cause effect) = do- pl <- gets splayer- void $ effectToAction effect 0 pl pl 0 False -- no block against tile- return ()- f (F.ChangeTo group) = do- Level{lactor} <- gets slevel- case lvl `atI` dloc of- [] -> if unoccupied (IM.elems lactor) dloc- then do- newTileId <- rndToAction $ opick group (const True)- let adj = (Kind.// [(dloc, newTileId)])- modify (updateLevel (updateLMap adj))--- TODO: take care of AI using this function (aborts, etc.).- else abortWith "blocked" -- by monsters or heroes- _ : _ -> abortWith "jammed" -- by items- f _ = return ()- mapM_ f $ TileKind.tfeature $ okind $ lvl `at` dloc---- | Ask for a direction and trigger a tile, if possible.-playerTriggerDir :: F.Feature -> MU.Part -> Action ()-playerTriggerDir feat verb = do- let keys = zip K.dirAllMoveKey $ repeat K.NoModifier- prompt = makePhrase ["What to", verb MU.:> "? [movement key"]- e <- displayChoiceUI prompt [] keys- lxsize <- gets (lxsize . slevel)- K.handleDir lxsize e (playerBumpDir feat) (neverMind True)---- | Player tries to trigger a tile in a given direction.-playerBumpDir :: F.Feature -> Vector -> Action ()-playerBumpDir feat dir = do- pl <- gets splayer- body <- gets (getActor pl)- let dloc = bloc body `shift` dir- bumpTile dloc feat---- | Player tries to trigger the tile he's standing on.-playerTriggerTile :: F.Feature -> Action ()-playerTriggerTile feat = do- ploc <- gets (bloc . getPlayerBody)- bumpTile ploc feat---- | An actor opens a door: player (hero or controlled monster) or enemy.-actorOpenDoor :: ActorId -> Vector -> Action ()-actorOpenDoor actor dir = do- Kind.COps{ cotile- , coitem- , coactor=Kind.Ops{okind}- } <- getCOps- lvl <- gets slevel- pl <- gets splayer- body <- gets (getActor actor)- bitems <- gets (getActorItem actor)- let dloc = shift (bloc body) dir -- the location we act upon- t = lvl `at` dloc- isPlayer = actor == pl- isVerbose = isPlayer -- don't report, unless it's player-controlled- iq = aiq $ okind $ bkind body- openPower = timeScale timeTurn $- if isPlayer- then 0 -- player can't open hidden doors- else case strongestSearch coitem bitems of- Just i -> iq + jpower i- Nothing -> iq- unless (openable cotile lvl openPower dloc) $ neverMind isVerbose- if Tile.hasFeature cotile F.Closable t- then abortIfWith isVerbose "already open"- else if not (Tile.hasFeature cotile F.Closable t ||- Tile.hasFeature cotile F.Openable t ||- Tile.hasFeature cotile F.Hidden t)- then neverMind isVerbose -- not doors at all- else triggerTile dloc---- | Change the displayed level in targeting mode to (at most)--- k levels shallower. Enters targeting mode, if not already in one.-tgtAscend :: Int -> ActionFrame ()-tgtAscend k = do- Kind.COps{cotile} <- getCOps- cursor <- gets scursor- targeting <- gets (ctargeting . scursor)- slid <- gets slid- lvl <- gets slevel- st <- get- dungeon <- gets sdungeon- let loc = clocation cursor- tile = lvl `at` loc- rightStairs =- k == 1 && Tile.hasFeature cotile (F.Cause Effect.Ascend) tile ||- k == -1 && Tile.hasFeature cotile (F.Cause Effect.Descend) tile- if rightStairs -- stairs, in the right direction- then case whereTo st k of- Nothing -> -- we are at the "end" of the dungeon- abortWith "no more levels in this direction"- Just (nln, nloc) ->- assert (nln /= slid `blame` (nln, "stairs looped")) $ do- -- We only look at the level, but we have to keep current- -- time somewhere, e.g., for when we change the player- -- to a hero on this level and then end targeting.- -- If that's too slow, we could keep current time in the @Cursor@.- switchLevel nln- -- do not freely reveal the other end of the stairs- lvl2 <- gets slevel- let upd cur =- let clocation =- if Tile.hasFeature cotile F.Exit (lvl2 `rememberAt` nloc)- then nloc -- already know as an exit, focus on it- else loc -- unknow, do not reveal the position- in cur { clocation, clocLn = nln }- modify (updateCursor upd)- else do -- no stairs in the right direction- let n = Dungeon.levelNumber slid- depth = Dungeon.depth dungeon- nln = Dungeon.levelDefault $ min depth $ max 1 $ n - k- when (nln == slid) $ abortWith "no more levels in this direction"- switchLevel nln -- see comment above- let upd cur = cur {clocLn = nln}- modify (updateCursor upd)- when (targeting == TgtOff) $ do- let upd cur = cur {ctargeting = TgtExplicit}- modify (updateCursor upd)- doLook--heroesAfterPl :: Action [ActorId]-heroesAfterPl = do- pl <- gets splayer- s <- get- let hs = map (tryFindHeroK s) [0..9]- i = fromMaybe (-1) $ L.findIndex (== Just pl) hs- (lt, gt) = (take i hs, drop (i + 1) hs)- return $ catMaybes gt ++ catMaybes lt---- | Switches current hero to the next hero on the level, if any, wrapping.--- We cycle through at most 10 heroes (\@, 1--9).-cycleHero :: Action ()-cycleHero = do- pl <- gets splayer- s <- get- hs <- heroesAfterPl- case L.filter (flip memActor s) hs of- [] -> abortWith "Cannot select any other hero on this level."- ni : _ -> selectPlayer ni- >>= assert `trueM` (pl, ni, "hero duplicated")---- | Switches current hero to the previous hero in the whole dungeon,--- if any, wrapping. We cycle through at most 10 heroes (\@, 1--9).-backCycleHero :: Action ()-backCycleHero = do- pl <- gets splayer- hs <- heroesAfterPl- case reverse hs of- [] -> abortWith "No other hero in the party."- ni : _ -> selectPlayer ni- >>= assert `trueM` (pl, ni, "hero duplicated")---- | Search for hidden doors.-search :: Action ()-search = do- Kind.COps{coitem, cotile} <- getCOps- lvl <- gets slevel- le <- gets (lsecret . slevel)- lxsize <- gets (lxsize . slevel)- ploc <- gets (bloc . getPlayerBody)- pitems <- gets getPlayerItem- let delta = timeScale timeTurn $- case strongestSearch coitem pitems of- Just i -> 1 + jpower i- Nothing -> 1- searchTile sle mv =- let loc = shift ploc mv- t = lvl `at` loc- -- TODO: assert or cope elsewhere with the IM.! below- k = timeAdd (le IM.! loc) $ timeNegate delta- in if Tile.hasFeature cotile F.Hidden t- then if k > timeZero- then IM.insert loc k sle- else IM.delete loc sle- else sle- leNew = L.foldl' searchTile le (moves lxsize)- modify (updateLevel (\ l -> l {lsecret = leNew}))- lvlNew <- gets slevel- let triggerHidden mv = do- let dloc = shift ploc mv- t = lvlNew `at` dloc- when (Tile.hasFeature cotile F.Hidden t && IM.notMember dloc leNew) $- triggerTile dloc- mapM_ triggerHidden (moves lxsize)---- | This function performs a move (or attack) by any actor,--- i.e., it can handle monsters, heroes and both.-moveOrAttack :: Bool -- ^ allow attacks?- -> ActorId -- ^ who's moving?- -> Vector -- ^ in which direction?- -> Action ()-moveOrAttack allowAttacks actor dir = do- -- We start by looking at the target position.- cops@Kind.COps{cotile = cotile@Kind.Ops{okind}} <- getCOps- state <- get- pl <- gets splayer- lvl <- gets slevel- sm <- gets (getActor actor)- let sloc = bloc sm -- source location- tloc = sloc `shift` dir -- target location- tgt <- gets (locToActor tloc)- case tgt of- Just target- | allowAttacks ->- -- Attacking does not require full access, adjacency is enough.- actorAttackActor actor target- | accessible cops lvl sloc tloc -> do- -- Switching positions requires full access.- when (actor == pl) $- msgAdd $ lookAt cops False True state lvl tloc ""- actorRunActor actor target- | otherwise -> abortWith "blocked"- Nothing- | accessible cops lvl sloc tloc -> do- -- Perform the move.- updateAnyActor actor $ \ body -> body {bloc = tloc}- when (actor == pl) $- msgAdd $ lookAt cops False True state lvl tloc ""- | allowAttacks && actor == pl- && Tile.canBeHidden cotile (okind $ lvl `rememberAt` tloc) -> do- msgAdd "You search all adjacent walls for half a second."- search- | otherwise ->- actorOpenDoor actor dir -- try to open a door, TODO: bumpTile tloc F.Openable---- | Resolves the result of an actor moving into another. Usually this--- involves melee attack, but with two heroes it just changes focus.--- Actors on blocked locations can be attacked without any restrictions.--- 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.-actorAttackActor :: ActorId -> ActorId -> Action ()-actorAttackActor source target = do- smRaw <- gets (getActor source)- tmRaw <- gets (getActor target)- per <- getPerception- time <- gets stime- sfaction <- gets sfaction- let sloc = bloc smRaw- tloc = bloc tmRaw- svisible = sloc `IS.member` totalVisible per- tvisible = tloc `IS.member` totalVisible per- sm | svisible = smRaw- | otherwise = smRaw {bname = Just "somebody"}- tm | tvisible = tmRaw- | otherwise = tmRaw {bname = Just "somebody"}- if bfaction sm == sfaction && not (bproj sm) &&- bfaction tm == sfaction && not (bproj tm)- then do- -- Select adjacent hero by bumping into him. Takes no time, so rewind.- selectPlayer target- >>= assert `trueM` (source, target, "heroes attack each other")- -- Mark that unexpectedly it does not take time.- modify (\ s -> s {stakeTime = Just False})- else do- cops@Kind.COps{coactor, coitem=coitem@Kind.Ops{opick, okind}} <- getCOps- state <- get- bitems <- gets (getActorItem source)- let h2hGroup = if isAHero state source then "unarmed" else "monstrous"- h2hKind <- rndToAction $ opick h2hGroup (const True)- let h2hItem = Item h2hKind 0 Nothing 1- (stack, tell, verbosity, verb) =- if isProjectile state source- then case bitems of- [bitem] -> (bitem, False, 10, "hit") -- projectile- _ -> assert `failure` bitems- else case strongestSword cops bitems of- Nothing -> (h2hItem, False, 0,- iverbApply $ okind $ h2hKind) -- hand to hand combat- Just w -> (w, True, 0,- iverbApply $ okind $ jkind w) -- weapon- -- The msg describes the source part of the action.- -- TODO: right now it also describes the victim and weapon;- -- perhaps, when a weapon is equipped, just say "you hit"- -- or "you miss" and then "nose dies" or "nose yells in pain".- msg = makeSentence $- [ MU.SubjectVerbSg (partActor coactor sm) verb- , partActor coactor tm ]- ++ if tell- then ["with", MU.AW $ partItem coitem state stack]- else []- msgMiss = makeSentence- [ MU.SubjectVerbSg (partActor coactor sm) "try to"- , verb MU.:> ", but"- , MU.SubjectVerbSg (partActor coactor tm) "block"- ]- let performHit block = do- when (svisible || tvisible) $ msgAdd msg- -- Msgs inside itemEffectAction describe the target part.- itemEffectAction verbosity source target stack block- -- Projectiles can't be blocked, can be sidestepped.- if braced tm time && not (bproj sm)- then do- blocked <- rndToAction $ chance $ 1%2- if blocked- then do- when (svisible || tvisible) $ msgAdd msgMiss- diary <- getDiary- let locs = (breturn tvisible tloc,- breturn svisible sloc)- anim = blockMiss locs- animFrs = animate state diary cops per anim- mapM_ displayFramePush $ Nothing : animFrs- else performHit True- else performHit False---- | Resolves the result of an actor running (not walking) into another.--- This involves switching positions of the two actors.-actorRunActor :: ActorId -> ActorId -> Action ()-actorRunActor source target = do- pl <- gets splayer- sm <- gets (getActor source)- tm <- gets (getActor target)- let sloc = bloc sm- tloc = bloc tm- updateAnyActor source $ \ m -> m { bloc = tloc }- updateAnyActor target $ \ m -> m { bloc = sloc }- cops@Kind.COps{coactor} <- getCOps- per <- getPerception- let visible = sloc `IS.member` totalVisible per ||- tloc `IS.member` totalVisible per- msg = makeSentence- [ MU.SubjectVerbSg (partActor coactor sm) "displace"- , partActor coactor tm ]- when visible $ msgAdd msg- diary <- getDiary -- here diary possibly contains the new msg- s <- get- let locs = (Just tloc, Just sloc)- animFrs = animate s diary cops per $ swapPlaces locs- when visible $ mapM_ displayFramePush $ Nothing : animFrs- if source == pl- then stopRunning -- do not switch positions repeatedly- else void $ focusIfOurs target---- | Create a new monster in the level, at a random position.-rollMonster :: Kind.COps -> Perception -> State -> Rnd State-rollMonster Kind.COps{ cotile- , coactor=Kind.Ops{opick, okind}- , cofact=Kind.Ops{opick=fopick, oname=foname}- } per state = do- let lvl@Level{lactor} = slevel state- ms = hostileList state- hs = heroList state- isLit = Tile.isLit cotile- rc <- monsterGenChance (Dungeon.levelNumber $ slid state) (L.length ms)- if not rc- then return state- else do- let distantAtLeast d =- \ l _ -> L.all (\ h -> chessDist (lxsize lvl) (bloc h) l > d) hs- loc <-- findLocTry 20 (lmap lvl) -- 20 only, for unpredictability- [ \ _ t -> not (isLit t)- , distantAtLeast 15- , \ l t -> not (isLit t) || distantAtLeast 15 l t- , distantAtLeast 10- , \ l _ -> not $ l `IS.member` totalVisible per- , distantAtLeast 5- , \ l t -> Tile.hasFeature cotile F.Walkable t- && unoccupied (IM.elems lactor) l- ]- bfaction <- fopick "spawn" (const True)- mk <- opick (foname bfaction) (const True)- hp <- rollDice $ ahp $ okind mk- return $ addMonster cotile mk hp loc bfaction False state---- | Generate a monster, possibly.-generateMonster :: Action ()-generateMonster = do- cops <- getCOps- state <- get- per <- getPerception- nstate <- rndToAction $ rollMonster cops per state- srandom <- gets srandom- put $ nstate {srandom}---- | Possibly regenerate HP for all actors on the current level.-regenerateLevelHP :: Action ()-regenerateLevelHP = do- Kind.COps{ coitem- , coactor=coactor@Kind.Ops{okind}- } <- getCOps- time <- gets stime- let upd itemIM a m =- let ak = okind $ bkind m- bitems = fromMaybe [] $ IM.lookup a itemIM- regen = max 1 $- aregen ak `div`- case strongestRegen coitem bitems of- Just i -> 5 * jpower i- Nothing -> 1- in if (time `timeFit` timeTurn) `mod` regen /= 0- then m- else addHp coactor 1 m- -- We really want hero selection to be a purely UI distinction,- -- so all heroes need to regenerate, not just the player.- -- Only the heroes on the current level regenerate (others are frozen- -- in time together with their level). This prevents cheating- -- via sending one hero to a safe level and waiting there.- hi <- gets (linv . slevel)- modify (updateLevel (updateActorDict (IM.mapWithKey (upd hi))))---- | Display command help.-displayHelp :: ActionFrame ()-displayHelp = do- keyb <- getBinding- displayOverlays "Basic keys." "[press SPACE to advance]" $ keyHelp keyb---- | Display the main menu.-displayMainMenu :: ActionFrame ()-displayMainMenu = do- Kind.COps{corule} <- getCOps- Binding{krevMap} <- getBinding- let stripFrame t = case T.uncons t of- Just ('\n', art) -> map (T.tail . T.init) $ tail . init $ T.lines art- _ -> assert `failure` "displayMainMenu:" <+> t- pasteVersion art =- let pathsVersion = rpathsVersion $ Kind.stdRuleset corule- version = " Version " ++ showVersion pathsVersion- ++ " (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 key = (showT key, Command.cmdDescription cmd)- revLookup cmd = maybe ("", "") (showKD cmd) $ M.lookup cmd krevMap- cmds = [Command.GameSave, Command.GameExit,- Command.GameRestart, Command.Help]- in map revLookup cmds ++ [(fst (revLookup Command.Clear), "continue")]- 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 . L.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"- hd : tl -> displayOverlays hd "" [tl]--displayHistory :: ActionFrame ()-displayHistory = do- Diary{shistory} <- getDiary- time <- gets stime- lysize <- gets (lysize . slevel)- let turn = time `timeFit` timeTurn- msg = makeSentence [ "You survived for"- , MU.NWs turn "half-second turn" ]- <+> "Past messages:"- displayOverlays msg "" $- splitOverlay lysize $ renderHistory shistory--dumpConfig :: Action ()-dumpConfig = do- ConfigUI{configRulesCfgFile} <- getConfigUI- let fn = configRulesCfgFile ++ ".dump"- msg = "Current game rules configuration dumped to file"- <+> T.pack fn <> "."- dumpCfg fn- abortWith msg---- | Add new smell traces to the level. Only humans leave a strong scent.-addSmell :: Action ()-addSmell = do- s <- get- pl <- gets splayer- let time = stime s- ploc = bloc (getPlayerBody s)- upd = IM.insert ploc $ timeAdd time $ smellTimeout s- when (isAHero s pl) $- modify $ updateLevel $ updateSmell upd---- | Update the wait/block count.-setWaitBlock :: ActorId -> Action ()-setWaitBlock actor = do- Kind.COps{coactor} <- getCOps- time <- gets stime- updateAnyActor actor $ \ m -> m {bwait = timeAddFromSpeed coactor m time}---- | Player waits a turn (and blocks, etc.).-waitBlock :: Action ()-waitBlock = do- pl <- gets splayer- setWaitBlock pl
− Game/LambdaHack/Actor.hs
@@ -1,198 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Actors in the game: monsters and heroes. No operation in this module--- involves the 'State' or 'Action' type.-module Game.LambdaHack.Actor- ( -- * Actor identifiers and related operations- ActorId, findHeroName, monsterGenChance, partActor- -- * The@ Acto@r type- , Actor(..), template, addHp, timeAddFromSpeed, braced- , unoccupied, heroKindId, projectileKindId, actorSpeed- -- * Type of na actor target- , Target(..)- ) where--import Control.Monad-import Data.Binary-import Data.Maybe-import Data.Ratio-import Data.Text (Text)-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Vector-import Game.LambdaHack.Point-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Content.FactionKind-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Msg-import Game.LambdaHack.Random-import Game.LambdaHack.Config-import Game.LambdaHack.Time-import qualified Game.LambdaHack.Color as Color---- | Actor properties that are changing throughout the game.--- If they are dublets of properties from @ActorKind@,--- 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 :: !(Maybe Char) -- ^ individual map symbol- , bname :: !(Maybe Text) -- ^ individual name- , bcolor :: !(Maybe Color.Color) -- ^ individual map color- , bspeed :: !(Maybe Speed) -- ^ individual speed- , bhp :: !Int -- ^ current hit points- , bdir :: !(Maybe (Vector, Int)) -- ^ direction and distance of running- , btarget :: Target -- ^ target for ranged attacks and AI- , bloc :: !Point -- ^ current location- , bletter :: !Char -- ^ next inventory letter- , btime :: !Time -- ^ absolute time of next action- , bwait :: !Time -- ^ last bracing expires at this time- , bfaction :: !(Kind.Id FactionKind) -- ^ to which faction the actor belongs- , bproj :: !Bool -- ^ is a projectile? (shorthand only,- -- ^ this can be deduced from bkind)- }- deriving Show--instance Binary Actor where- put Actor{..} = do- put bkind- put bsymbol- put bname- put bcolor- put bspeed- put bhp- put bdir- put btarget- put bloc- put bletter- put btime- put bwait- put bfaction- put bproj- get = do- bkind <- get- bsymbol <- get- bname <- get- bcolor <- get- bspeed <- get- bhp <- get- bdir <- get- btarget <- get- bloc <- get- bletter <- get- btime <- get- bwait <- get- bfaction <- get- bproj <- get- return Actor{..}---- ActorId operations---- | A unique identifier of an actor in a dungeon.-type ActorId = Int---- | Find a hero name in the config file, or create a stock name.-findHeroName :: ConfigUI -> Int -> Text-findHeroName ConfigUI{configHeroNames} n =- let heroName = lookup n configHeroNames- in fromMaybe ("hero number" <+> showT n) heroName---- | Chance that a new monster is generated. Currently depends on the--- number of monsters already present, and on the level. In the future,--- the strength of the character and the strength of the monsters present--- could further influence the chance, and the chance could also affect--- which monster is generated. How many and which monsters are generated--- will also depend on the cave kind used to build the level.-monsterGenChance :: Int -> Int -> Rnd Bool-monsterGenChance depth numMonsters =- chance $ 1%(fromIntegral (30 * (numMonsters - depth)) `max` 5)---- | The part of speech describing the actor.-partActor :: Kind.Ops ActorKind -> Actor -> MU.Part-partActor Kind.Ops{oname} a = MU.Text $ fromMaybe (oname $ bkind a) (bname a)---- Actor operations---- | A template for a new non-projectile actor. The initial target is invalid--- to force a reset ASAP.-template :: Kind.Id ActorKind -> Maybe Char -> Maybe Text -> Int -> Point- -> Time -> Kind.Id FactionKind -> Bool -> Actor-template bkind bsymbol bname bhp bloc btime bfaction bproj =- let bcolor = Nothing- bspeed = Nothing- btarget = invalidTarget- bdir = Nothing- bletter = 'a'- bwait = timeZero- in Actor{..}---- | Increment current hit points of an actor.-addHp :: Kind.Ops ActorKind -> Int -> Actor -> Actor-addHp Kind.Ops{okind} extra m =- assert (extra >= 0 `blame` extra) $- let maxHP = maxDice (ahp $ okind $ bkind m)- currentHP = bhp m- in if currentHP > maxHP- then m- else m {bhp = min maxHP (currentHP + extra)}---- | Access actor speed, individual or, otherwise, stock.-actorSpeed :: Kind.Ops ActorKind -> Actor -> Speed-actorSpeed Kind.Ops{okind} m =- let stockSpeed = aspeed $ okind $ bkind m- in fromMaybe stockSpeed $ bspeed m---- | Add time taken by a single step at the actor's current speed.-timeAddFromSpeed :: Kind.Ops ActorKind -> Actor -> Time -> Time-timeAddFromSpeed coactor m time =- let speed = actorSpeed coactor m- delta = ticksPerMeter speed- in timeAdd time delta---- | Whether an actor is braced for combat this turn.-braced :: Actor -> Time -> Bool-braced m time = time < bwait m---- | Checks for the presence of actors in a location.--- Does not check if the tile is walkable.-unoccupied :: [Actor] -> Point -> Bool-unoccupied actors loc =- all (\ body -> bloc body /= loc) actors---- | The unique kind of heroes.-heroKindId :: Kind.Ops ActorKind -> Kind.Id ActorKind-heroKindId Kind.Ops{ouniqGroup} = ouniqGroup "hero"---- | The unique kind of projectiles.-projectileKindId :: Kind.Ops ActorKind -> Kind.Id ActorKind-projectileKindId Kind.Ops{ouniqGroup} = ouniqGroup "projectile"---- Target---- | The type of na actor target.-data Target =- TEnemy ActorId Point -- ^ target an actor with its last seen location- | TLoc Point -- ^ target a given location- | TPath [Vector] -- ^ target the list of locations one after another- | TCursor -- ^ target current position of the cursor; default- deriving (Show, Eq)---- | An invalid target, with an actor that is not on any level.-invalidTarget :: Target-invalidTarget =- let invalidActorId = -1- in TEnemy invalidActorId origin--instance Binary Target where- put (TEnemy a ll) = putWord8 0 >> put a >> put ll- put (TLoc loc) = putWord8 1 >> put loc- put (TPath ls) = putWord8 2 >> put ls- put TCursor = putWord8 3- get = do- tag <- getWord8- case tag of- 0 -> liftM2 TEnemy get get- 1 -> liftM TLoc get- 2 -> liftM TPath get- 3 -> return TCursor- _ -> fail "no parse (Target)"
− Game/LambdaHack/ActorState.hs
@@ -1,307 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Operations on the 'Actor' type that need the 'State' type,--- but not the 'Action' type.--- TODO: Document an export list after it's rewritten according to #17.-module Game.LambdaHack.ActorState- ( isProjectile, isAHero, getPlayerBody, findActorAnyLevel, calculateTotal- , smellTimeout, initialHeroes, deletePlayer, allHeroesAnyLevel- , locToActor, deleteActor, addHero, addMonster, updateAnyActorItem- , insertActor, heroList, memActor, getActor, updateAnyActorBody- , hostileList, getActorItem, getPlayerItem, tryFindHeroK, dangerousList- , factionList, addProjectile, foesAdjacent, targetToLoc, hostileAssocs- ) where--import Control.Monad-import qualified Data.List as L-import qualified Data.IntSet as IS-import qualified Data.IntMap as IM-import Data.Maybe-import qualified Data.Char as Char-import Data.Text (Text)-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Point-import Game.LambdaHack.PointXY-import Game.LambdaHack.Vector-import Game.LambdaHack.Actor-import Game.LambdaHack.Level-import Game.LambdaHack.Dungeon-import Game.LambdaHack.State-import Game.LambdaHack.Msg-import Game.LambdaHack.Item-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Content.FactionKind-import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Config-import qualified Game.LambdaHack.Tile as Tile-import qualified Game.LambdaHack.Kind as Kind-import qualified Game.LambdaHack.Feature as F-import Game.LambdaHack.Time---- | Checks whether an actor identifier represents a hero.-isProjectile :: State -> ActorId -> Bool-isProjectile s a =- let (_, actor, _) = findActorAnyLevel a s- in bproj actor---- TODO: currently it's false for player-controlled monsters.--- When it's no longer, rewrite the places where it matters.--- | Checks whether an actor identifier represents a hero.-isAHero :: State -> ActorId -> Bool-isAHero s a =- let (_, actor, _) = findActorAnyLevel a s- in bfaction actor == sfaction s && not (bproj actor)---- TODO: move to TileState if ever created.--- | How long until an actor's smell vanishes from a tile.-smellTimeout :: State -> Time-smellTimeout s =- let Config{configSmellTimeout} = sconfig s- in timeScale timeTurn configSmellTimeout---- The operations with "Any", and those that use them,--- consider all the dungeon.--- All the other actor and level operations only consider the current level.---- | Finds an actor body on any level. Fails if not found.-findActorAnyLevel :: ActorId -> State -> (LevelId, Actor, [Item])-findActorAnyLevel actor State{slid, sdungeon} =- let chk (ln, lvl) =- let (m, mi) = (IM.lookup actor (lactor lvl),- IM.lookup actor (linv lvl))- in fmap (\ a -> (ln, a, fromMaybe [] mi)) m- in case mapMaybe chk (currentFirst slid sdungeon) of- [] -> assert `failure` actor- res : _ -> res -- checking if res is unique would break laziness---- | Tries to finds an actor body satisfying a predicate on any level.-tryFindActor :: State -> (Actor -> Bool) -> Maybe (ActorId, Actor)-tryFindActor State{slid, sdungeon} p =- let chk (_ln, lvl) = L.find (p . snd) $ IM.assocs $ lactor lvl- in case mapMaybe chk (currentFirst slid sdungeon) of- [] -> Nothing- res : _ -> Just res--getPlayerBody :: State -> Actor-getPlayerBody s@State{splayer} =- let (_, actor, _) = findActorAnyLevel splayer s- in actor--getPlayerItem :: State -> [Item]-getPlayerItem s@State{splayer} =- let (_, _, items) = findActorAnyLevel splayer s- in items---- | The list of actors and their levels for all heroes in the dungeon.-allHeroesAnyLevel :: State -> [ActorId]-allHeroesAnyLevel State{slid, sdungeon, sfaction} =- let one (_, lvl) =- [ a | (a, m) <- IM.toList $ lactor lvl- , bfaction m == sfaction && not (bproj m) ]- in L.concatMap one (currentFirst slid sdungeon)--updateAnyActorBody :: ActorId -> (Actor -> Actor) -> State -> State-updateAnyActorBody actor f state =- let (ln, _, _) = findActorAnyLevel actor state- in updateAnyLevel (updateActorDict $ IM.adjust f actor) ln state--updateAnyActorItem :: ActorId -> ([Item] -> [Item]) -> State -> State-updateAnyActorItem actor f state =- let (ln, _, _) = findActorAnyLevel actor state- g Nothing = Just $ f []- g (Just is) = Just $ f is- in updateAnyLevel (updateInv $ IM.alter g actor) ln state--updateAnyLevel :: (Level -> Level) -> LevelId -> State -> State-updateAnyLevel f ln s@State{slid, sdungeon}- | ln == slid = updateLevel f s- | otherwise = updateDungeon (const $ adjust f ln sdungeon) s---- | Calculate the location of player's target.-targetToLoc :: IS.IntSet -> State -> Point -> Maybe Point-targetToLoc visible s@State{slid, scursor} aloc =- case btarget (getPlayerBody s) of- TLoc loc -> Just loc- TPath [] -> Nothing- TPath (dir:_) -> Just $ shift aloc dir- TCursor ->- if slid == clocLn scursor- then Just $ clocation scursor- else Nothing -- cursor invalid: set at a different level- TEnemy a _ll -> do- guard $ memActor a s -- alive and on the current level?- let loc = bloc (getActor a s)- guard $ IS.member loc visible -- visible?- return loc---- The operations below disregard levels other than the current.---- | Checks if the actor is present on the current level.--- The order of argument here and in other functions is set to allow------ > b <- gets (memActor a)-memActor :: ActorId -> State -> Bool-memActor a state = IM.member a (lactor (slevel state))---- | Gets actor body from the current level. Error if not found.-getActor :: ActorId -> State -> Actor-getActor a state = lactor (slevel state) IM.! a---- | Gets actor's items from the current level. Empty list, if not found.-getActorItem :: ActorId -> State -> [Item]-getActorItem a state = fromMaybe [] $ IM.lookup a (linv (slevel state))---- | Removes the actor, if present, from the current level.-deleteActor :: ActorId -> State -> State-deleteActor a =- updateLevel (updateActorDict (IM.delete a) . updateInv (IM.delete a))---- | Add actor to the current level.-insertActor :: ActorId -> Actor -> State -> State-insertActor a m = updateLevel (updateActorDict (IM.insert a m))---- | Removes a player from the current level.-deletePlayer :: State -> State-deletePlayer s@State{splayer} = deleteActor splayer s---- TODO: unify, rename-hostileAssocs :: Kind.Id FactionKind -> Level -> [(ActorId, Actor)]-hostileAssocs faction lvl =- filter (\ (_, m) -> bfaction m /= faction && not (bproj m)) $- IM.toList $ lactor lvl-heroList, hostileList, dangerousList :: State -> [Actor]-heroList state@State{sfaction} =- filter (\ m -> bfaction m == sfaction && not (bproj m)) $- IM.elems $ lactor $ slevel state-hostileList state@State{sfaction} =- filter (\ m -> bfaction m /= sfaction && not (bproj m)) $- IM.elems $ lactor $ slevel state-dangerousList state@State{sfaction} =- filter (\ m -> bfaction m /= sfaction) $- IM.elems $ lactor $ slevel state--factionAssocs :: [Kind.Id FactionKind] -> Level -> [(ActorId, Actor)]-factionAssocs l lvl =- filter (\ (_, m) -> bfaction m `elem` l) $ IM.toList $ lactor lvl--factionList :: [Kind.Id FactionKind] -> State -> [Actor]-factionList l s =- filter (\ m -> bfaction m `elem` l) $ IM.elems $ lactor $ slevel s---- | Finds an actor at a location on the current level. Perception irrelevant.-locToActor :: Point -> State -> Maybe ActorId-locToActor loc state =- let l = locToActors loc state- in assert (L.length l <= 1 `blame` l) $- listToMaybe l--locToActors :: Point -> State -> [ActorId]-locToActors loc state =- let l = IM.assocs $ lactor $ slevel state- im = L.filter (\ (_i, m) -> bloc m == loc) l- in fmap fst im--nearbyFreeLoc :: Kind.Ops TileKind -> Point -> State -> Point-nearbyFreeLoc cotile start state =- let lvl@Level{lxsize, lysize, lactor} = slevel state- locs = start : L.nub (concatMap (vicinity lxsize lysize) locs)- good loc = Tile.hasFeature cotile F.Walkable (lvl `at` loc)- && unoccupied (IM.elems lactor) loc- in fromMaybe (assert `failure` ("too crowded map" :: Text))- $ L.find good locs---- | Calculate loot's worth for heroes on the current level.-calculateTotal :: Kind.Ops ItemKind -> State -> ([Item], Int)-calculateTotal coitem s =- let ha = factionAssocs [sfaction s] $ slevel s- heroInv = L.concat $ catMaybes $- L.map ( \ (k, _) -> IM.lookup k $ linv $ slevel s) ha- in (heroInv, L.sum $ L.map (itemPrice coitem) heroInv)--foesAdjacent :: X -> Y -> Point -> [Actor] -> Bool-foesAdjacent lxsize lysize loc foes =- let vic = IS.fromList $ vicinity lxsize lysize loc- lfs = IS.fromList $ L.map bloc foes- in not $ IS.null $ IS.intersection vic lfs---- Adding heroes--tryFindHeroK :: State -> Int -> Maybe ActorId-tryFindHeroK s k =- let c | k == 0 = '@'- | k > 0 && k < 10 = Char.intToDigit k- | otherwise = assert `failure` k- in fmap fst $ tryFindActor s ((== Just c) . bsymbol)---- | Create a new hero on the current level, close to the given location.-addHero :: Kind.COps -> Point -> ConfigUI -> State -> State-addHero Kind.COps{coactor, cotile} ploc configUI- state@State{scounter, sfaction} =- let Config{configBaseHP} = sconfig state- loc = nearbyFreeLoc cotile ploc state- freeHeroK = L.elemIndex Nothing $ map (tryFindHeroK state) [0..9]- n = fromMaybe 100 freeHeroK- symbol = if n < 1 || n > 9 then '@' else Char.intToDigit n- name = findHeroName configUI n- startHP = configBaseHP - (configBaseHP `div` 5) * min 3 n- m = template (heroKindId coactor) (Just symbol) (Just name)- startHP loc (stime state) sfaction False- cstate = state { scounter = scounter + 1 }- in updateLevel (updateActorDict (IM.insert scounter m)) cstate---- | Create a set of initial heroes on the current level, at location ploc.-initialHeroes :: Kind.COps -> Point -> ConfigUI -> State -> State-initialHeroes cops ploc configUI state =- let Config{configExtraHeroes} = sconfig state- k = 1 + configExtraHeroes- in iterate (addHero cops ploc configUI) state !! k---- Adding monsters---- | Create a new monster in the level, at a given position--- and with a given actor kind and HP.-addMonster :: Kind.Ops TileKind -> Kind.Id ActorKind -> Int -> Point- -> Kind.Id FactionKind -> Bool -> State -> State-addMonster cotile mk hp ploc bfaction bproj state@State{scounter} = do- let loc = nearbyFreeLoc cotile ploc state- m = template mk Nothing Nothing hp loc (stime state) bfaction bproj- cstate = state {scounter = scounter + 1}- updateLevel (updateActorDict (IM.insert scounter m)) cstate---- Adding projectiles---- | Create a projectile actor containing the given missile.-addProjectile :: Kind.COps -> Item -> Point -> Kind.Id FactionKind- -> [Point] -> Time -> State -> State-addProjectile Kind.COps{coactor, coitem=coitem@Kind.Ops{okind}}- item loc bfaction path btime state@State{scounter} =- let ik = okind (jkind item)- speed = speedFromWeight (iweight ik) (itoThrow ik)- range = rangeFromSpeed speed- adj | range < 5 = "falling"- | otherwise = "flying"- object = partItem coitem state item- name = makePhrase [MU.AW $ MU.Text adj, object]- dirPath = take range $ displacePath path- m = Actor- { bkind = projectileKindId coactor- , bsymbol = Nothing- , bname = Just name- , bcolor = Nothing- , bspeed = Just speed- , bhp = 0- , bdir = Nothing- , btarget = TPath dirPath- , bloc = loc- , bletter = 'a'- , btime- , bwait = timeZero- , bfaction- , bproj = True- }- cstate = state { scounter = scounter + 1 }- upd = updateActorDict (IM.insert scounter m)- . updateInv (IM.insert scounter [item])- in updateLevel upd cstate
− Game/LambdaHack/Animation.hs
@@ -1,128 +0,0 @@--- | Screen frames and animations.-module Game.LambdaHack.Animation- ( Attr(..), defaultAttr, AttrChar(..)- , SingleFrame(..), Animation, rederAnim- , twirlSplash, blockHit, blockMiss, deathBody, swapPlaces- ) where--import qualified Data.IntMap as IM-import Data.Maybe-import qualified Data.List as L-import Data.Monoid-import Data.Text (Text)--import Game.LambdaHack.PointXY-import Game.LambdaHack.Point-import Game.LambdaHack.Color---- | The data sufficent to draw a single game screen frame.-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- }- deriving Eq---- | Animation is a list of frame modifications to play one by one,--- where each modification if a map from locations to level map symbols.-newtype Animation = Animation [IM.IntMap AttrChar]--instance Monoid Animation where- mempty = Animation []- mappend (Animation a1) (Animation a2) = Animation (a1 ++ a2)---- | Render animations on top of a screen frame.-rederAnim :: X -> Y -> SingleFrame -> Animation- -> [Maybe SingleFrame]-rederAnim lxsize lysize basicFrame (Animation anim) =- let modifyFrame SingleFrame{sfLevel = levelOld, ..} am =- let fLine y lineOld =- let f l (x, acOld) =- let loc = toPoint lxsize (PointXY (x, y))- !ac = fromMaybe acOld $ IM.lookup loc am- in ac : l- in L.foldl' f [] (zip [lxsize-1,lxsize-2..0] (reverse lineOld))- sfLevel = -- Fully evaluated.- 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 (modifyFrame basicFrame) anim--blank :: Maybe AttrChar-blank = Nothing--coloredSymbol :: Color -> Char -> Maybe AttrChar-coloredSymbol color symbol = Just $ AttrChar (Attr color defBG) symbol--mzipPairs :: (Maybe Point, Maybe Point) -> (Maybe AttrChar, Maybe AttrChar)- -> [(Point, AttrChar)]-mzipPairs (mloc1, mloc2) (mattr1, mattr2) =- let mzip (Just loc, Just attr) = Just (loc, attr)- mzip _ = Nothing- in if mloc1 /= mloc2- then catMaybes [mzip (mloc1, mattr1), mzip (mloc2, mattr2)]- else -- If actor affects himself, show only the effect, not the action.- catMaybes [mzip (mloc1, mattr1)]---- | Attack animation. A part of it also reused for self-damage and healing.-twirlSplash :: (Maybe Point, Maybe Point) -> Color -> Color -> Animation-twirlSplash locs c1 c2 = Animation $ map (IM.fromList . mzipPairs locs)- [ (coloredSymbol BrWhite '*', blank)- , (coloredSymbol c1 '/', coloredSymbol BrCyan '^')- , (coloredSymbol c1 '-', blank)- , (coloredSymbol c1 '\\',blank)- , (coloredSymbol c1 '|', blank)- , (coloredSymbol c2 '/', blank)- , (coloredSymbol c2 '%', coloredSymbol BrCyan '^')- , (coloredSymbol c2 '%', blank)- , (blank , blank)- ]---- | Attack that hits through a block.-blockHit :: (Maybe Point, Maybe Point) -> Color -> Color -> Animation-blockHit locs c1 c2 = Animation $ map (IM.fromList . mzipPairs locs)- [ (coloredSymbol BrWhite '*', blank)- , (coloredSymbol BrBlue '{', coloredSymbol BrCyan '^')- , (coloredSymbol BrBlue '{', blank)- , (coloredSymbol c1 '}', blank)- , (coloredSymbol c1 '}', coloredSymbol BrCyan '^')- , (coloredSymbol c2 '/', blank)- , (coloredSymbol c2 '%', blank)- , (coloredSymbol c2 '%', blank)- , (blank , blank)- ]---- | Attack that is blocked.-blockMiss :: (Maybe Point, Maybe Point) -> Animation-blockMiss locs = Animation $ map (IM.fromList . mzipPairs locs)- [ (coloredSymbol BrWhite '*', blank)- , (coloredSymbol BrBlue '{', coloredSymbol BrCyan '^')- , (coloredSymbol BrBlue '}', blank)- , (coloredSymbol BrBlue '}', blank)- , (blank , blank)- ]---- | Death animation for an organic body.-deathBody :: Point -> Animation-deathBody loc = Animation $ map (maybe IM.empty (IM.singleton loc))- [ coloredSymbol BrRed '\\'- , coloredSymbol BrRed '\\'- , coloredSymbol BrRed '|'- , coloredSymbol BrRed '|'- , coloredSymbol BrRed '%'- , coloredSymbol BrRed '%'- , coloredSymbol Red '%'- , coloredSymbol Red '%'- , coloredSymbol Red ';'- , coloredSymbol Red ';'- , coloredSymbol Red ','- ]---- | Swap-places animation, both hostile and friendly.-swapPlaces :: (Maybe Point, Maybe Point) -> Animation-swapPlaces locs = Animation $ map (IM.fromList . mzipPairs locs)- [ (coloredSymbol BrMagenta '.', coloredSymbol Magenta 'o')- , (coloredSymbol BrMagenta 'd', coloredSymbol Magenta 'p')- , (coloredSymbol Magenta 'p', coloredSymbol BrMagenta 'd')- , (coloredSymbol Magenta 'o', blank)- ]
− Game/LambdaHack/Area.hs
@@ -1,62 +0,0 @@--- | Rectangular areas of levels and their basic operations.-module Game.LambdaHack.Area- ( Area, vicinityXY, vicinityCardinalXY, insideXY- , normalizeArea, grid, validArea, trivialArea, expand- ) where--import Game.LambdaHack.PointXY-import Game.LambdaHack.VectorXY---- | The type of areas. The bottom left and the top right points.-type Area = (X, Y, X, Y)---- | All (8 at most) closest neighbours of a point within an area.-vicinityXY :: Area -- ^ limit the search to this area- -> PointXY -- ^ location 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 :: Area -- ^ limit the search to this area- -> PointXY -- ^ location to find neighbours of- -> [PointXY]-vicinityCardinalXY area xy =- [ res- | dxy <- movesCardinalXY, let res = shiftXY xy dxy, insideXY res area ]---- | Checks that a point belongs to an area.-insideXY :: PointXY -> Area -> Bool-insideXY (PointXY (x, y)) (x0, y0, x1, y1) =- x1 >= x && x >= x0 && y1 >= y && y >= y0---- | Sort the corners of an area so that the bottom left is the first point.-normalizeArea :: Area -> Area-normalizeArea (x0, y0, x1, y1) = (min x0 x1, min y0 y1, max x0 x1, max y0 y1)---- | Divide uniformly a larger area into the given number of smaller areas.-grid :: (X, Y) -> Area -> [(PointXY, Area)]-grid (nx, ny) (x0, y0, x1, y1) =- let xd = x1 - x0- yd = y1 - y0- -- 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.- xborder = if nx == 1 then 3 else 2- yborder = if ny == 1 then 3 else 2- in [ (PointXY (x, y), (x0 + (xd * x `div` nx) + xborder,- y0 + (yd * y `div` ny) + yborder,- x0 + (xd * (x + 1) `div` nx) - xborder,- y0 + (yd * (y + 1) `div` ny) - yborder))- | x <- [0..nx-1], y <- [0..ny-1] ]---- | Checks if it's an area with at least one field.-validArea :: Area -> Bool-validArea (x0, y0, x1, y1) = x0 <= x1 && y0 <= y1---- | Checks if it's an area with exactly one field.-trivialArea :: Area -> Bool-trivialArea (x0, y0, x1, y1) = x0 == x1 && y0 == y1---- | Enlarge (or shrink) the given area on all fours sides by the amount.-expand :: Area -> Int -> Area-expand (x0, y0, x1, y1) k = (x0 - k, y0 - k, x1 + k, y1 + k)
− Game/LambdaHack/AreaRnd.hs
@@ -1,145 +0,0 @@--- | Operations on the 'Area' type that involve random numbers.-module Game.LambdaHack.AreaRnd- ( -- * Picking points inside areas- xyInArea, mkRoom, mkVoidRoom- -- * Choosing connections- , connectGrid, randomConnection- -- * Plotting corridors- , Corridor, connectPlaces- ) where--import qualified Data.Set as S--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.PointXY-import Game.LambdaHack.Area-import Game.LambdaHack.Random---- Picking random points inside areas---- | Pick a random point within an area.-xyInArea :: Area -> Rnd PointXY-xyInArea (x0, y0, x1, y1) = do- rx <- randomR (x0, x1)- ry <- randomR (y0, y1)- return $ PointXY (rx, ry)---- | Create a random room according to given parameters.-mkRoom :: (X, Y) -- ^ minimum size- -> Area -- ^ the containing area, not the room itself- -> Rnd Area-mkRoom (xm, ym) (x0, y0, x1, y1) = do- let area0 = (x0, y0, x1 - xm + 1, y1 - ym + 1)- assert (validArea area0 `blame` area0) $ do- PointXY (rx0, ry0) <- xyInArea area0- let area1 = (rx0 + xm - 1, ry0 + ym - 1, x1, y1)- assert (validArea area1 `blame` area1) $ do- PointXY (rx1, ry1) <- xyInArea area1- return (rx0, ry0, rx1, ry1)---- | Create a void room, i.e., a single point area.-mkVoidRoom :: Area -- ^ the area in which to pick the point- -> Rnd Area-mkVoidRoom area = assert (validArea area `blame` area) $ do- PointXY (ry, rx) <- xyInArea area- return (ry, rx, ry, rx)---- 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 (nx, ny) = do- let unconnected = S.fromList [ PointXY (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 = S.fromList [ PointXY (rx, ry) ]- connectGrid' (nx, ny) unconnected candidates []--connectGrid' :: (X, Y) -> S.Set PointXY -> S.Set PointXY- -> [(PointXY, PointXY)]- -> Rnd [(PointXY, PointXY)]-connectGrid' (nx, ny) unconnected candidates acc- | S.null candidates = return $ map sortPointXY acc- | otherwise = do- c <- oneOf (S.toList candidates)- -- potential new candidates:- let ns = S.fromList $ vicinityCardinalXY (0, 0, nx-1, ny-1) c- nu = S.delete c unconnected -- new unconnected- -- (new candidates, potential connections):- (nc, ds) = S.partition (`S.member` nu) ns- new <- if S.null ds- then return id- else do- d <- oneOf (S.toList ds)- return ((c, d) :)- connectGrid' (nx, ny) nu (S.delete c (candidates `S.union` nc)) (new acc)---- | Pick a single random connection between adjacent areas within a grid.-randomConnection :: (X, Y) -> Rnd (PointXY, PointXY)-randomConnection (nx, ny) =- assert (nx > 1 && ny > 0 || nx > 0 && ny > 1 `blame` (nx, ny)) $ do- rb <- oneOf [False, True]- if rb || ny <= 1- then do- rx <- randomR (0, nx-2)- ry <- randomR (0, ny-1)- return (PointXY (rx, ry), PointXY (rx+1, ry))- else do- rx <- randomR (0, nx-1)- ry <- randomR (0, ny-2)- return (PointXY (rx, ry), PointXY (rx, ry+1))---- Plotting individual corridors between two areas---- | The choice of horizontal and vertical orientation.-data HV = Horiz | Vert---- | The coordinates of consecutive fields of a corridor.-type Corridor = [PointXY]---- | 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- -> 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)]---- TODO: assert that sx1 <= tx0, etc.--- | Try to connect two places with a corridor.--- Choose entrances at least 4 tiles distant from the edges, if possible.-connectPlaces :: Area -> Area -> Rnd Corridor-connectPlaces sa@(_, _, sx1, sy1) ta@(tx0, ty0, _, _) = do- let trim (x0, y0, x1, y1) =- let trim4 (v0, v1) = if v1 - v0 < 8 then (v0, v1) else (v0 + 4, v1 - 4)- (nx0, nx1) = trim4 (x0, x1)- (ny0, ny1) = trim4 (y0, y1)- in (nx0, ny0, nx1, ny1)- PointXY (sx, sy) <- xyInArea $ trim sa- PointXY (tx, ty) <- xyInArea $ trim ta- let xarea = (sx1+2, min sy ty, tx0-2, max sy ty)- yarea = (sx, sy1+2, tx, ty0-2)- xyarea = (sx1+2, sy1+2, tx0-2, ty0-2)- (hv, area) <- if validArea xyarea- then fmap (\ hv -> (hv, xyarea)) (oneOf [Horiz, Vert])- else if validArea xarea- then return (Horiz, xarea)- else return (Vert, normalizeArea yarea) -- vertical bias- let (p0, p1) = case hv of- Horiz -> (PointXY (if trivialArea sa then sx else sx1 + 1, sy),- PointXY (if trivialArea ta then tx else tx0 - 1, ty))- Vert -> (PointXY (sx, if trivialArea sa then sy else sy1 + 1),- PointXY (tx, if trivialArea ta then ty else ty0 - 1))- -- 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.- mkCorridor hv p0 p1 area
− Game/LambdaHack/Binding.hs
@@ -1,87 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Generic binding of keys to commands, procesing macros,--- printing command help. No operation in this module--- involves the 'State' or 'Action' type.-module Game.LambdaHack.Binding- ( Binding(..), keyHelp,- ) where--import qualified Data.Map as M-import qualified Data.List as L-import qualified Data.Set as S-import Data.Text (Text)-import qualified Data.Text as T--import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Msg-import qualified Game.LambdaHack.Command as Command---- | Bindings and other information about player commands.-data Binding a = Binding- { kcmd :: M.Map (K.Key, K.Modifier) (Text, Bool, a)- -- ^ binding keys to commands- , kmacro :: M.Map K.Key K.Key -- ^ macro map- , kmajor :: [K.Key] -- ^ major, most often used, commands- , kdir :: [(K.Key, K.Modifier)] -- ^ direction keys for moving and running- , krevMap :: M.Map Command.Cmd K.Key- -- ^ map from cmds to their main keys- }--coImage :: M.Map K.Key K.Key -> K.Key -> [K.Key]-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 a -> [Overlay]-keyHelp Binding{kcmd, kmacro, kmajor} =- let- movBlurb =- [ "Move throughout the level with 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"- , ""- ,"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 monsters."- , ""- , "Press SPACE to see the next page, with the list of major commands."- ]- majorBlurb =- [ ""- , "Commands marked with * take time and are blocked on remote levels."- , "Press SPACE to see the next page, with the list of minor commands."- ]- minorBlurb =- [ ""- , "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- keyCaption = fmt "keys" "command"- disp k = T.concat $ map showT $ coImage kmacro k- keys l = [ fmt (disp k) (h <> if timed then "*" else "")- | ((k, _), (h, timed, _)) <- l, h /= "" ]- (kcMajor, kcMinor) =- L.partition ((`elem` kmajor) . fst . fst) (M.toAscList kcmd)- in- [ [blank] ++ mov- , [blank] ++ [keyCaption] ++ keys kcMajor ++ major- , [blank] ++ [keyCaption] ++ keys kcMinor ++ minor- ]
− Game/LambdaHack/BindingAction.hs
@@ -1,73 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Binding of keys to commands implemented with the 'Action' monad.-module Game.LambdaHack.BindingAction- ( stdBinding- ) where--import Control.Monad.State hiding (State, state)-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Char as Char-import Data.Tuple (swap)-import Data.Text (Text)--import Game.LambdaHack.Action-import Game.LambdaHack.State-import Game.LambdaHack.Config-import Game.LambdaHack.Level-import Game.LambdaHack.Actions-import Game.LambdaHack.Running-import Game.LambdaHack.EffectAction-import Game.LambdaHack.Binding-import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.ActorState-import Game.LambdaHack.Command-import Game.LambdaHack.CommandAction--heroSelection :: [((K.Key, K.Modifier), (Text, Bool, ActionFrame ()))]-heroSelection =- let select k = do- s <- get- case tryFindHeroK s k of- Nothing -> abortWith "No such member of the party."- Just aid -> selectPlayer aid >> returnNoFrame ()- heroSelect k = ( (K.Char (Char.intToDigit k), K.NoModifier)- , ("", False, select k)- )- in fmap heroSelect [0..9]---- | Binding of keys to movement and other standard commands,--- as well as commands defined in the config file.-stdBinding :: ConfigUI -- ^ game config- -> Binding (ActionFrame ()) -- ^ concrete binding-stdBinding !config@ConfigUI{configMacros} =- let kmacro = M.fromList $ configMacros- cmdList = configCmds config- semList = semanticsCmds cmdList- moveWidth f = do- lxsize <- gets (lxsize . slevel)- move $ f lxsize- runWidth f = do- lxsize <- gets (lxsize . slevel)- run (f lxsize, 0)- -- Targeting cursor movement and others are wrongly marked as timed;- -- fixed in their definitions by rewinding time.- cmdDir = K.moveBinding moveWidth runWidth- in Binding- { kcmd = M.fromList $- cmdDir ++- heroSelection ++- semList ++- [ -- Debug commands.- ((K.Char 'r', K.Control), ("", False, modify cycleMarkVision- >> returnNoFrame ())),- ((K.Char 'o', K.Control), ("", False, modify toggleOmniscient- >> returnNoFrame ())),- ((K.Char 'i', K.Control), ("", False, gets (lmeta . slevel)- >>= abortWith))- ]- , kmacro- , kmajor = L.map fst $ L.filter (majorCmd . snd) cmdList- , kdir = L.map fst cmdDir- , krevMap = M.fromList $ map swap cmdList- }
− Game/LambdaHack/CDefs.hs
@@ -1,23 +0,0 @@--- | A game requires the engine provided by the library, perhaps customized,--- and game content, defined completely afresh for the particular game.--- The general type of the content is @CDefs@ and it has instances--- for all content kinds, such as items kinds--- (@Game.LambdaHack.Content.ItemKind@).--- The possible kinds are fixed in the library and all defined in the same--- directory. On the other hand, game content, that is all elements--- of @CDefs@ instances, are defined in a directory--- of the game code proper, with names corresponding to their kinds.-module Game.LambdaHack.CDefs (CDefs(..)) where--import Data.Text (Text)--import Game.LambdaHack.Misc---- | The general type of a particular game content, e.g., item kinds.-data CDefs a = CDefs- { getSymbol :: a -> Char -- ^ symbol, e.g., to print on the map- , getName :: a -> Text -- ^ name, e.g., to show to the player- , getFreq :: a -> Freqs -- ^ frequency within groups- , validate :: [a] -> [a] -- ^ validate and catch some offenders, if any- , content :: [a] -- ^ all the defined content of this type- }
− Game/LambdaHack/Cave.hs
@@ -1,206 +0,0 @@--- | Generation of caves (not yet inhabited dungeon levels) from cave kinds.-module Game.LambdaHack.Cave- ( TileMapXY, SecretMapXY, ItemMapXY, Cave(..), buildCave- ) where--import Control.Monad-import qualified Data.Map as M-import qualified Data.List as L-import Data.Maybe-import Data.Text (Text)--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.PointXY-import Game.LambdaHack.Area-import Game.LambdaHack.AreaRnd-import Game.LambdaHack.Item-import Game.LambdaHack.Random-import qualified Game.LambdaHack.Tile as Tile-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Content.CaveKind-import Game.LambdaHack.Content.TileKind-import qualified Game.LambdaHack.Feature as F-import Game.LambdaHack.Place hiding (TileMapXY)-import qualified Game.LambdaHack.Place as Place-import Game.LambdaHack.Misc-import Game.LambdaHack.Time-import Game.LambdaHack.Msg---- | The map of tile kinds in a cave.--- The map is sparse. The default tile that eventually fills the empty spaces--- is specified in the cave kind specification with @cdefaultTile@.-type TileMapXY = Place.TileMapXY---- | The map of starting secrecy strength of tiles in a cave.--- The map is sparse. Unspecified tiles have secrecy strength of 0.-type SecretMapXY = M.Map PointXY Tile.SecretTime---- | The map of starting items in tiles of a cave. The map is sparse.--- Unspecified tiles have no starting items.-type ItemMapXY = M.Map PointXY Item---- | 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- , dsecret :: SecretMapXY -- ^ secrecy strength of cave tiles- , ditem :: ItemMapXY -- ^ starting items in the cave- , dmeta :: Text -- ^ debug information about the cave- , dplaces :: [Place] -- ^ places generated in the cave- }- deriving Show--{--Rogue cave is generated by an algorithm inspired by the original Rogue,-as follows:-- * The available area is divided into a grid, e.g, 3 by 3,- where each of the 9 grid cells has approximately the same size.-- * In each of the 9 grid cells one room is placed at a random location- and with a random size, but larger than The minimum size,- e.g, 2 by 2 floor tiles.-- * Rooms that are on horizontally or vertically adjacent grid cells- may be connected by a corridor. Corridors consist of 3 segments of straight- lines (either "horizontal, vertical, horizontal" or "vertical, horizontal,- vertical"). They end in openings in the walls of the room they connect.- It is possible that one or two of the 3 segments have length 0, such that- the resulting corridor is L-shaped or even a single straight line.-- * Corridors are generated randomly in such a way that at least every room- on the grid is connected, and a few more might be. It is not sufficient- to always connect all adjacent rooms.--}--- TODO: fix identifier naming and split, after the code grows some more--- | Cave generation by an algorithm inspired by the original Rogue,-buildCave :: Kind.COps -- ^ content definitions- -> Int -- ^ depth of the level to generate- -> 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{okind=tokind, opick}- , cocave=Kind.Ops{okind} }- ln depth ci = do- let kc@CaveKind{..} = okind ci- lgrid@(gx, gy) <- rollDiceXY cgrid- lminplace <- rollDiceXY cminPlaceSize- let gs = grid lgrid (0, 0, cxsize - 1, cysize - 1)- mandatory1 <- replicateM (cnonVoidMin `div` 2) $- xyInArea (0, 0, gx `div` 3, gy - 1)- mandatory2 <- replicateM (cnonVoidMin `divUp` 2) $- xyInArea (gx - 1 - (gx `div` 3), 0, gx - 1, gy - 1)- places0 <- mapM (\ (i, r) -> do- rv <- chance cvoidChance- r' <- if rv && i `notElem` (mandatory1 ++ mandatory2)- then mkVoidRoom r- else mkRoom lminplace r- return (i, r')) gs- connects <- connectGrid lgrid- addedConnects <-- if gx * gy > 1- then let caux = round $ cauxConnects * fromIntegral (gx * gy)- in replicateM caux (randomConnection lgrid)- else return []- let allConnects = L.union connects addedConnects -- no duplicates- places = M.fromList places0- cs <- mapM (\ (p0, p1) -> do- let r0 = places M.! p0- r1 = places M.! p1- connectPlaces r0 r1) allConnects- wallId <- opick cfillerTile (const True)- let fenceBounds = (1, 1, cxsize - 2, cysize - 2)- fence = buildFence wallId fenceBounds- pickedCorTile <- opick ccorridorTile (const True)- let addPl (m, pls) (_, (x0, _, x1, _)) | x0 == x1 = return (m, pls)- addPl (m, pls) (_, r) = do- (tmap, place) <- buildPlace cops kc pickedCorTile ln depth r- return (M.union tmap m, place : pls)- (lplaces, dplaces) <- foldM addPl (fence, []) places0- let lcorridors = M.unions (L.map (digCorridors pickedCorTile) cs)- hiddenMap <- mapToHidden cotile chiddenTile- let lm = M.unionWith (mergeCorridor cotile hiddenMap) lcorridors lplaces- -- Convert openings into doors, possibly.- (dmap, secretMap) <-- let f (l, le) (p, t) =- if Tile.hasFeature cotile F.Hidden t- then do- -- Openings have a certain chance to be doors;- -- doors have a certain chance to be open; and- -- closed doors have a certain chance to be hidden- rd <- chance cdoorChance- if not rd- then return (M.insert p pickedCorTile l, le)- else do- doorClosedId <- trigger cotile t- doorOpenId <- trigger cotile doorClosedId- ro <- chance copenChance- if ro- then return (M.insert p doorOpenId l, le)- else do- rs <- chance chiddenChance- if not rs- then return (M.insert p doorClosedId l, le)- else do- secret <- rollSecret (tokind t)- return (l, M.insert p secret le)- else return (l, le)- in foldM f (lm, M.empty) (M.toList lm)- let cave = Cave- { dkind = ci- , dsecret = secretMap- , ditem = M.empty- , dmap- , dmeta = showT allConnects- , dplaces- }- return cave--rollSecret :: TileKind -> Rnd Tile.SecretTime-rollSecret t = do- let getDice (F.Secret dice) _ = dice- getDice _ acc = acc- defaultDice = RollDice 5 2- d = foldr getDice defaultDice (tfeature t)- secretTurns <- rollDice d- return $ timeScale timeTurn secretTurns--trigger :: Kind.Ops TileKind -> Kind.Id TileKind -> Rnd (Kind.Id TileKind)-trigger Kind.Ops{okind, opick} t =- let getTo (F.ChangeTo group) _ = Just group- getTo _ acc = acc- in case foldr getTo Nothing (tfeature (okind t)) of- Nothing -> return t- Just group -> opick group (const True)--digCorridors :: Kind.Id TileKind -> Corridor -> TileMapXY-digCorridors tile (p1:p2:ps) =- M.union corPos (digCorridors tile (p2:ps))- where- corXY = fromTo p1 p2- corPos = M.fromList $ L.zip corXY (repeat tile)-digCorridors _ _ = M.empty--passable :: [F.Feature]-passable = [F.Walkable, F.Openable, F.Hidden]--mapToHidden :: Kind.Ops TileKind -> Text- -> Rnd (M.Map (Kind.Id TileKind) (Kind.Id TileKind))-mapToHidden cotile@Kind.Ops{ofoldrWithKey, opick} chiddenTile =- let getHidden ti tk acc =- if Tile.canBeHidden cotile tk- then do- ti2 <- opick chiddenTile $ \ k -> Tile.kindHasFeature F.Hidden k- && Tile.similar k tk- fmap (M.insert ti ti2) acc- else acc- in ofoldrWithKey getHidden (return M.empty)--mergeCorridor :: Kind.Ops TileKind- -> M.Map (Kind.Id TileKind) (Kind.Id TileKind)- -> Kind.Id TileKind -> Kind.Id TileKind -> Kind.Id TileKind-mergeCorridor cotile _ _ t- | L.any (\ f -> Tile.hasFeature cotile f t) passable = t-mergeCorridor _ hiddenMap u t =- fromMaybe (assert `failure` (u, hiddenMap, t)) $- M.lookup t hiddenMap
+ Game/LambdaHack/Client.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Semantics of client commands.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Client+ ( cmdClientAISem, cmdClientUISem+ , loopAI, loopUI, exeFrontend+ , MonadClient, MonadClientUI, MonadConnClient+ ) where++import Data.Maybe++import Game.LambdaHack.Client.Action+import Game.LambdaHack.Client.AtomicSemCli+import Game.LambdaHack.Client.Binding+import Game.LambdaHack.Client.ClientSem+import Game.LambdaHack.Client.Config+import Game.LambdaHack.Client.LoopAction+import Game.LambdaHack.Client.State+import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.ClientCmd+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.State+import Game.LambdaHack.Frontend+import Game.LambdaHack.Utils.Assert++storeUndo :: MonadClient m => Atomic -> m ()+storeUndo _atomic =+ maybe skip (\a -> modifyClient $ \cli -> cli {sundo = a : sundo cli})+ $ Nothing -- TODO: undoAtomic atomic++cmdClientAISem :: (MonadAtomic m, MonadConnClient c m)+ => CmdClientAI -> m ()+cmdClientAISem cmd = case cmd of+ CmdAtomicAI cmdA -> do+ cmds <- cmdAtomicFilterCli cmdA+ mapM_ cmdAtomicSemCli cmds+ mapM_ execCmdAtomic cmds+ mapM_ (storeUndo . CmdAtomic) cmds+ CmdQueryAI aid -> do+ cmdC <- queryAI aid+ ConnServer{writeConnServer} <- getConn+ writeConnServer cmdC++cmdClientUISem :: ( MonadAtomic m, MonadClientAbort m+ , MonadClientUI m, MonadConnClient c 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_ (storeUndo . CmdAtomic) cmds -- TODO: only store cmdA?+ SfxAtomicUI sfx -> do+ drawSfxAtomicUI False sfx+ storeUndo $ SfxAtomic sfx+ CmdQueryUI aid -> do+ mleader <- getsClient _sleader+ assert (isJust mleader `blame` cmd) skip+ ConnServer{writeConnServer} <- getConn+ cmdH <- queryUI aid+ writeConnServer cmdH++wireSession :: (SessionUI -> State -> StateClient+ -> ConnServer CmdClientUI -> IO ())+ -> (SessionUI -> State -> StateClient+ -> ConnServer CmdClientAI -> IO ())+ -> Kind.COps+ -> ((FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ())+ -> (FactionId -> ChanServer CmdClientAI -> IO ())+ -> IO ())+ -> IO ()+wireSession exeClientUI exeClientAI cops@Kind.COps{corule} exeServer = do+ -- UI config reloaded at each client start.+ sconfigUI <- mkConfigUI corule+ let !sbinding = stdBinding sconfigUI -- evaluate to check for errors+ font = configFont sconfigUI+ defHist <- defHistory+ let cli = defStateClient defHist sconfigUI+ s = updateCOps (const cops) emptyState+ executorAI fid chanS =+ let noSession = assert `failure` fid+ in exeClientAI noSession s (cli fid True) (connServer chanS)+ executorUI fid fromF chanS =+ let sfconn = connFrontend fid fromF+ in exeClientUI SessionUI{..} s (cli fid False) (connServer chanS)+ startupF font $ exeServer executorUI executorAI++-- | 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+ , MonadConnClient CmdClientUI m+ , MonadAtomic n+ , MonadConnClient CmdClientAI n )+ => (m () -> SessionUI -> State -> StateClient+ -> ConnServer CmdClientUI+ -> IO ())+ -> (n () -> SessionUI -> State -> StateClient+ -> ConnServer CmdClientAI+ -> IO ())+ -> Kind.COps+ -> ((FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ())+ -> (FactionId -> ChanServer CmdClientAI -> IO ())+ -> IO ())+ -> IO ()+exeFrontend executorUI executorAI cops exeServer = do+ let loopClientUI = loopUI cmdClientUISem+ loopClientAI = loopAI cmdClientAISem+ exeClientUI = executorUI loopClientUI+ exeClientAI = executorAI loopClientAI+ wireSession exeClientUI exeClientAI cops exeServer
+ Game/LambdaHack/Client/Action.hs view
@@ -0,0 +1,428 @@+{-# LANGUAGE OverloadedStrings #-}+-- | 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+-- details.+module Game.LambdaHack.Client.Action+ ( -- * Action monads+ MonadClient( getClient, getsClient, putClient, modifyClient )+ , MonadClientUI+ , MonadConnClient( getConn )+ , MonadClientAbort( abortWith, tryWith )+ , SessionUI(..), ConnFrontend(..), connFrontend+ , ConnServer(..), connServer+ -- * 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)+ , askBinding, getPerFid+ -- * History and report+ , msgAdd, msgReset, recordHistory+ -- * Key input+ , getKeyOverlayCommand, getInitConfirms+ -- * Display and key input+ , displayFrames, displayMore, displayYesNo, displayChoiceUI+ -- * Generate slideshows+ , promptToSlideshow, overlayToSlideshow+ -- * Draw frames+ , drawOverlay, animate+ -- * Assorted primitives+ , clientGameSave, restoreGame, displayPush, scoreToSlideshow+ , rndToAction, getArenaUI, getLeaderUI+ , targetToPos, partAidLeader, partActorLeader+ , debugPrint+ ) where++import Control.Concurrent+import Control.Concurrent.STM+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.Map.Strict as M+import Data.Maybe+import qualified Data.Monoid as Monoid+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import qualified NLP.Miniutter.English as MU+import System.IO (hFlush, stderr)+import qualified System.Random as R+import System.Time++import Game.LambdaHack.Client.Action.ActionClass+import Game.LambdaHack.Client.Action.ConfigIO+import qualified Game.LambdaHack.Client.Action.Save as Save+import Game.LambdaHack.Client.Binding+import Game.LambdaHack.Client.Config+import Game.LambdaHack.Client.Draw+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 Game.LambdaHack.Common.ClientCmd+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.HighScore as HighScore+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.State+import Game.LambdaHack.Content.RuleKind+import qualified Game.LambdaHack.Frontend as Frontend+import Game.LambdaHack.Utils.Assert++debugPrint :: MonadClient m => Text -> m ()+debugPrint t = do+ debug <- getsClient sdebugCli+ when debug $ liftIO $ do+ delay <- R.randomRIO (0, 1000000)+ threadDelay delay -- try not to interleave letters with other clients+ T.hPutStrLn stderr t+ hFlush stderr++connFrontend :: FactionId -> Frontend.ChanFrontend -> ConnFrontend+connFrontend fid fromF = ConnFrontend+ { readConnFrontend =+ liftIO $ atomically $ readTQueue fromF+ , writeConnFrontend = \efr -> do+ let toF = Frontend.toMulti Frontend.connMulti+ liftIO $ atomically $ writeTQueue toF (fid, efr)+ }++connServer :: ChanServer c -> ConnServer c+connServer ChanServer{..} = ConnServer+ { readConnServer = liftIO . atomically . readTQueue $ fromServer+ , writeConnServer = liftIO . atomically . writeTQueue toServer+ }++-- | 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` msg <+> "in tryIgnore")++-- | 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+ let frame = case mf of+ Nothing -> AcDelay+ Just fr | isRunning -> AcRunning fr+ Just fr -> AcNormal fr+ writeConnFrontend $ Frontend.FrontFrame frame++promptGetKey :: MonadClientUI m => [K.KM] -> SingleFrame -> m K.KM+promptGetKey frontKM frontFr = do+ ConnFrontend{..} <- getsSession sfconn+ writeConnFrontend Frontend.FrontKey {..}+ readConnFrontend++-- | Display a slideshow, awaiting confirmation for each slide except the last.+getInitConfirms :: MonadClientUI m => [K.KM] -> Slideshow -> m Bool+getInitConfirms frontClear slides = do+ ConnFrontend{..} <- getsSession sfconn+ frontSlides <- mapM (drawOverlay ColorFull) $ runSlideshow slides+ -- The first two cases are optimizations:+ case frontSlides of+ [] -> return True+ [x] -> do+ displayFrame False $ Just x+ return True+ _ -> do+ writeConnFrontend Frontend.FrontSlides{..}+ km <- readConnFrontend+ return $! km /= K.KM {key=K.Esc, modifier=K.NoModifier}++getLeaderUI :: MonadClientUI m => m ActorId+getLeaderUI = do+ cli <- getClient+ case _sleader cli of+ Nothing -> assert `failure` 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+ dungeon <- getsState sdungeon+ case EM.minViewWithKey dungeon of+ Just ((s, _), _) -> return s+ Nothing -> assert `failure` dungeon++-- | 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++-- | Add a message to the current report.+msgAdd :: MonadClientUI m => Msg -> m ()+msgAdd msg = modifyClient $ \d -> d {sreport = addMsg (sreport d) msg}++-- | Wipe out and set a new value for the current report.+msgReset :: MonadClient m => Msg -> m ()+msgReset msg = modifyClient $ \d -> d {sreport = singletonReport msg}++-- | Store current report in the history and reset report.+recordHistory :: MonadClient m => m ()+recordHistory = do+ StateClient{sreport, shistory} <- getClient+ unless (nullReport sreport) $ do+ ConfigUI{configHistoryMax} <- getsClient sconfigUI+ msgReset ""+ let nhistory = takeHistory configHistoryMax $! addReport sreport shistory+ modifyClient $ \cli -> cli {shistory = nhistory}++-- | Get the current perception of a client.+getPerFid :: MonadClient m => LevelId -> m Perception+getPerFid lid = do+ fper <- getsClient sfper+ return $! fromMaybe (assert `failure` lid) $ 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+ -- Give the previous client time to display his frames.+ liftIO $ threadDelay 1000+ km <- promptGetKey [] frame+ return $! fromMaybe km $ M.lookup km $ kmacro keyb++getConfirm :: MonadClientUI m => [K.KM] -> SingleFrame -> m Bool+getConfirm = Frontend.getConfirmGeneric promptGetKey++-- | Push frames or delays to the frame queue.+displayFrames :: MonadClientUI m => Frames -> m ()+displayFrames = mapM_ (displayFrame False)++-- | A yes-no confirmation.+getYesNo :: MonadClientUI m => SingleFrame -> m Bool+getYesNo frame = do+ let keys = [ K.KM {key=K.Char 'y', modifier=K.NoModifier}+ , K.KM {key=K.Char 'n', modifier=K.NoModifier}+ , K.KM {key=K.Esc, modifier=K.NoModifier}+ ]+ K.KM {key} <- promptGetKey keys frame+ case key of+ K.Char 'y' -> return True+ _ -> return False++-- | Display a msg with a @more@ prompt. Return value indicates if the player+-- tried to cancel/escape.+displayMore :: MonadClientUI m => ColorMode -> Msg -> m Bool+displayMore dm prompt = do+ sli <- promptToSlideshow $ prompt <+> moreMsg+ frame <- drawOverlay dm $ head $ runSlideshow sli+ getConfirm [] frame++-- | 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+ 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 prompt ov keys = do+ slides <- fmap runSlideshow $ overlayToSlideshow (prompt <> ", ESC]") ov+ let legalKeys =+ [ K.KM {key=K.Space, modifier=K.NoModifier}+ , K.KM {key=K.Esc, modifier=K.NoModifier} ]+ ++ keys+ loop [] = neverMind True+ loop (x : xs) = do+ frame <- drawOverlay ColorFull x+ km@K.KM {..} <- promptGetKey legalKeys frame+ case key of+ K.Esc -> neverMind True+ K.Space -> loop xs+ _ -> return km+ loop slides++-- | 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 []++-- | 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+-- to history. The portions of overlay that fit on the the rest+-- of the screen are displayed below. As many slides as needed are shown.+overlayToSlideshow :: MonadClientUI m => Msg -> Overlay -> m Slideshow+overlayToSlideshow prompt overlay = do+ lid <- getArenaUI+ lysize <- getsLevel lid lysize -- TODO: screen length or viewLevel+ sreport <- getsClient sreport+ let msg = splitReport (addMsg sreport prompt)+ return $! splitOverlay lysize msg overlay++-- | Draw the current level with the overlay on top.+drawOverlay :: MonadClientUI m => ColorMode -> Overlay -> m SingleFrame+drawOverlay dm over = do+ cops <- getsState scops+ stgtMode <- getsClient stgtMode+ arena <- getArenaUI+ let lid = maybe arena tgtLevelId stgtMode+ mleader <- getsClient _sleader+ s <- getState+ cli <- getClient+ per <- getPerFid lid+ return $! draw dm cops per lid mleader cli s over++-- | Push the frame depicting the current level to the frame queue.+-- Only one screenful of the report is shown, the rest is ignored.+displayPush :: MonadClientUI m => m ()+displayPush = do+ sls <- promptToSlideshow ""+ let slide = head $ runSlideshow sls+ frame <- drawOverlay 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+ displayFrame (isJust srunning) $ Just frame++scoreToSlideshow :: MonadClientUI m => Int -> Status -> m Slideshow+scoreToSlideshow total status = do+ 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++saveName :: FactionId -> Bool -> String+saveName side isAI =+ let n = fromEnum side+ in (if n > 0+ then "human_" ++ show n+ else "computer_" ++ show (-n))+ ++ if isAI then ".ai.sav" else ".ui.sav"++clientGameSave :: MonadClient m => Bool -> m ()+clientGameSave toBkp = do+ s <- getState+ cli <- getClient+ configUI <- getsClient sconfigUI+ side <- getsClient sside+ isAI <- getsClient sisAI+ liftIO $ Save.saveGameCli (saveName side isAI) toBkp configUI s cli++restoreGame :: MonadClient m => m (Either (State, StateClient, Msg) Msg)+restoreGame = do+ Kind.COps{corule} <- getsState scops+ configUI <- getsClient sconfigUI+ let pathsDataFile = rpathsDataFile $ Kind.stdRuleset corule+ title = rtitle $ Kind.stdRuleset corule+ side <- getsClient sside+ isAI <- getsClient sisAI+ let sName = saveName side isAI+ liftIO $ Save.restoreGameCli sName configUI pathsDataFile title++-- | Invoke pseudo-random computation with the generator kept in the state.+rndToAction :: MonadClient m => Rnd a -> m a+rndToAction r = do+ g <- getsClient srandom+ let (a, ng) = St.runState r g+ modifyClient $ \cli -> cli {srandom = ng}+ return a++-- TODO: perhaps draw viewed level, not arena+-- TODO: restrict the animation to 'per' before drawing.+-- | Render animations on top of the current screen frame.+animate :: MonadClientUI m => LevelId -> Animation -> m Frames+animate arena anim = do+ cops <- getsState scops+ sreport <- getsClient sreport+ mleader <- getsClient _sleader+ Level{lxsize, lysize} <- getsLevel arena id+ cli <- getClient+ s <- getState+ per <- getPerFid arena+ let over = renderReport sreport+ topLineOnly = truncateMsg lxsize over+ basicFrame = draw ColorFull cops per arena mleader cli s [topLineOnly]+ return $ 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.+partActorLeader :: MonadClient m => ActorId -> Actor -> m MU.Part+partActorLeader aid b = do+ Kind.COps{coactor} <- getsState scops+ mleader <- getsClient _sleader+ return $! case mleader of+ Just leader | aid == leader -> "you"+ _ -> partActor coactor b++-- | The part of speech describing the actor (designated by actor id+-- and present in the dungeon) or a special name if a leader+-- of the observer's faction.+partAidLeader :: MonadClient m => ActorId -> m MU.Part+partAidLeader aid = do+ b <- getsState $ getActorBody aid+ partActorLeader aid b
+ Game/LambdaHack/Client/Action/ActionClass.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE FunctionalDependencies, RankNTypes #-}+-- | Basic type classes for game actions.+-- This module should not be imported anywhere except in 'Action'+-- 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.Msg+import Game.LambdaHack.Common.ServerCmd+import Game.LambdaHack.Frontend (FrontReq)++-- | The information that is constant across a client playing session,+-- including many consecutive games in a single session,+-- but is completely disregarded and reset when a new playing session starts.+-- Auxiliary AI and computer player clients have no @sfs@ nor @sbinding@.+data SessionUI = SessionUI+ { sfconn :: !ConnFrontend -- ^ connection with the frontend+ , sbinding :: !Binding -- ^ binding of keys to commands+ }++-- | Connection method between a client and a frontend.+data ConnFrontend = ConnFrontend+ { readConnFrontend :: MonadClientUI m => m K.KM+ -- ^ read a keystroke received from the frontend+ , writeConnFrontend :: MonadClientUI m => FrontReq -> m ()+ -- ^ write a UI request to the frontend+ }++data ConnServer c = ConnServer+ { readConnServer :: MonadConnClient c m => m c+ , writeConnServer :: MonadConnClient c m => CmdSer -> m ()+ }++class MonadActionRO m => MonadClient m where+ getClient :: m StateClient+ getsClient :: (StateClient -> a) -> m a+ modifyClient :: (StateClient -> StateClient) -> m ()+ putClient :: StateClient -> m ()+ -- We do not provide a MonadIO instance, so that outside of Action/+ -- nobody can subvert the action monads by invoking arbitrary IO.+ liftIO :: IO a -> m a++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++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 => MonadConnClient c m | m -> c where+ getConn :: m (ConnServer c)++-- | 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
+ Game/LambdaHack/Client/Action/ActionType.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, OverloadedStrings #-}+-- | 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+ ) where++import qualified Data.EnumMap.Strict as EM+import qualified Data.Text as T+import qualified System.Random as R++import Game.LambdaHack.Client.Action.ActionClass+import Game.LambdaHack.Client.State+import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.State++-- | The type of the function inside any client action.+type FunActionCli c a =+ SessionUI -- ^ client UI setup data+ -> ConnServer c -- ^ 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 a = ActionCli {runActionCli :: FunActionCli c a}++-- | Invokes the action continuation on the provided argument.+returnActionCli :: a -> ActionCli c 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 a -> (a -> ActionCli c b) -> ActionCli c 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) where+ return = returnActionCli+ (>>=) = bindActionCli++-- TODO: make sure fmap is inlined and all else is inlined here and elsewhere+instance Functor (ActionCli c) where+ fmap f m =+ ActionCli (\c d k a s cli ->+ runActionCli m c d (\s' cli' ->+ k s' cli' . f) a s cli)++instance Show (ActionCli c a) where+ show _ = "an action"++instance MonadClientAbort (ActionCli c) 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)++instance MonadActionRO (ActionCli c) where+ getState = ActionCli (\_c _d k _a s cli -> k s cli s)+ getsState = (`fmap` getState)++instance MonadAction (ActionCli c) where+ modifyState f = ActionCli (\_c _d k _a s cli -> k (f s) cli ())+ putState = modifyState . const++instance MonadClient (ActionCli c) 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)++instance MonadClientUI (ActionCli c) where+ getsSession f = ActionCli (\c _d k _a s cli -> k s cli (f c))++instance MonadConnClient c (ActionCli c) where+ getConn = ActionCli (\_c d k _a s cli -> k s cli d)++-- | Run an action, with a given session, state and history, in the @IO@ monad.+executorCli :: ActionCli c ()+ -> SessionUI -> State -> StateClient -> ConnServer c+ -> IO ()+executorCli m sess s cli d =+ runActionCli m+ sess+ d+ (\_ _ _ -> return ())+ (\_ msg -> let err = "unhandled abort for client"+ <+> showT (sfactionD s EM.! sside cli)+ <+> ":" <+> msg+ in fail $ T.unpack err)+ s+ cli
+ Game/LambdaHack/Client/Action/ConfigIO.hs view
@@ -0,0 +1,122 @@+-- | Personal game configuration file support.+module Game.LambdaHack.Client.Action.ConfigIO+ ( mkConfigUI+ ) where++import Control.DeepSeq+import qualified Data.Char as Char+import qualified Data.ConfigFile as CF+import System.Directory+import System.Environment+import System.FilePath++import Game.LambdaHack.Client.Config+import Game.LambdaHack.Client.HumanCmd+import qualified Game.LambdaHack.Common.Key as K+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Utils.Assert++-- TODO: Refactor the client and server ConfigIO.hs, after+-- https://github.com/kosmikus/LambdaHack/issues/45.++overrideCP :: CP -> FilePath -> IO CP+overrideCP cp@(CP defCF) cfile = do+ b <- doesFileExist cfile+ if not b+ then return cp+ else do+ c <- CF.readfile defCF cfile+ return $ toCP $ forceEither c++-- | Read a player configuration file and use it to override+-- options from a default config. Currently we can't unset options,+-- only override. The default config, passed in argument @configDefault@,+-- is expected to come from a default configuration file included via CPP.+-- The player configuration comes from file @cfile@.+mkConfig :: String -> FilePath -> IO CP+mkConfig configDefault cfile = do+ let delComment = map (drop 2) $ init . drop 3 $ lines configDefault+ unConfig = unlines delComment+ -- Evaluate, to catch config errors ASAP.+ !defCF = 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++-- | The content of the configuration file. It's parsed+-- in a case sensitive way (unlike by default in ConfigFile).+newtype CP = CP CF.ConfigParser++instance Show CP where+ show (CP conf) = show $ CF.to_string conf++-- | Switches all names to case sensitive (unlike by default in+-- the "ConfigFile" library) and wraps in the constructor.+toCP :: CF.ConfigParser -> CP+toCP cf = CP $ cf {CF.optionxform = id}++-- | In case of corruption, just fail.+forceEither :: Show a => Either a b -> b+forceEither (Left a) = assert `failure` a+forceEither (Right b) = b++-- | Simplified access to an option in a given section.+-- Fails if the option is not present.+get :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> a+get (CP conf) s o =+ if CF.has_option conf s o+ then forceEither $ CF.get conf s o+ else assert `failure` "Unknown config option: " ++ s ++ "." ++ o++-- | An association list corresponding to a section. Fails if no such section.+getItems :: CP -> CF.SectionSpec -> [(String, String)]+getItems (CP conf) s =+ if CF.has_section conf s+ then forceEither $ CF.items conf s+ else assert `failure` "Unknown config section: " ++ s++parseConfigUI :: FilePath -> CP -> ConfigUI+parseConfigUI dataDir cp =+ let mkKey s =+ case K.keyTranslate s of+ K.Unknown _ ->+ assert `failure` ("unknown config file key <" ++ s ++ ">")+ key -> key+ 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 = getItems cp "commands"+ in map mkCommand section+ configAppDataDirUI = dataDir+ configUICfgFile = dataDir </> "config.ui"+ configMacros =+ let trMacro (from, to) =+ let fromTr = mkKM from+ toTr = mkKM to+ in if fromTr == toTr+ then assert `failure` "degenerate alias: " ++ show toTr+ else (fromTr, toTr)+ section = getItems cp "macros"+ in map trMacro section+ configFont = get cp "ui" "font"+ configHistoryMax = get cp "ui" "historyMax"+ in ConfigUI{..}++-- | Read and parse UI config file.+mkConfigUI :: Kind.Ops RuleKind -> IO ConfigUI+mkConfigUI corule = do+ let cpUIDefault = rcfgUIDefault $ Kind.stdRuleset corule+ appData <- appDataDir+ cpUI <- mkConfig cpUIDefault $ appData </> "config.ui.ini"+ let conf = parseConfigUI appData cpUI+ -- Catch syntax errors ASAP,+ return $! deepseq conf conf
+ Game/LambdaHack/Client/Action/Save.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Saving and restoring client game state.+module Game.LambdaHack.Client.Action.Save+ ( saveGameCli, restoreGameCli+ ) where++import Control.Concurrent+import qualified Control.Exception as Ex hiding (handle)+import Control.Monad+import Data.Text (Text)+import qualified Data.Text as T+import System.Directory+import System.FilePath+import System.IO.Unsafe (unsafePerformIO)++import Game.LambdaHack.Client.Config+import Game.LambdaHack.Client.State+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.State+import Game.LambdaHack.Utils.File++-- TODO: Refactor the client and server Save.hs, after+-- https://github.com/kosmikus/LambdaHack/issues/37.++_saveLock :: MVar ()+{-# NOINLINE _saveLock #-}+_saveLock = unsafePerformIO newEmptyMVar++-- | Save game to the backup savefile, in case of crashes.+-- This is only a backup, so no problem is the game is shut down+-- before saving finishes, so we don't wait on the mvar. However,+-- if a previous save is already in progress, we skip this save.+saveGameBkpCli :: String -> ConfigUI -> State -> StateClient -> IO ()+saveGameBkpCli saveName ConfigUI{configAppDataDirUI} s cli = do+-- b <- tryPutMVar saveLock ()+-- when b $+-- void $ forkIO $ do+ let saveFile = configAppDataDirUI </> saveName+ saveFileBkp = saveFile <.> ".bkp"+ encodeEOF saveFile (s, cli)+ renameFile saveFile saveFileBkp+-- takeMVar saveLock++-- | Save a simple serialized version of the current state.+-- Protected by a lock to avoid corrupting the file.+saveGameCli :: String -> Bool -> ConfigUI -> State -> StateClient -> IO ()+saveGameCli saveName True configUI s cl =+ saveGameBkpCli saveName configUI s cl+saveGameCli saveName False ConfigUI{configAppDataDirUI} s cli = do+-- putMVar saveLock ()+ let saveFile = configAppDataDirUI </> saveName+ encodeEOF saveFile (s, cli)+-- takeMVar saveLock++-- | Restore a saved game, if it exists. Initialize directory structure,+-- if needed.+restoreGameCli :: String -> ConfigUI -> (FilePath -> IO FilePath) -> Text+ -> IO (Either (State, StateClient, Msg) Msg)+restoreGameCli saveName ConfigUI{ configAppDataDirUI+ , configUICfgFile }+ pathsDataFile title = do+ tryCreateDir configAppDataDirUI+ tryCopyDataFiles pathsDataFile+ [(configUICfgFile <.> ".default", configUICfgFile <.> ".ini")]+ let saveFile = configAppDataDirUI </> saveName+ saveFileBkp = saveFile <.> ".bkp"+ sb <- doesFileExist saveFile+ bb <- doesFileExist saveFileBkp+ when sb $ renameFile saveFile saveFileBkp+ -- If the savefile exists but we get IO or decoding errors, we show them,+ -- back up the savefile, move it out of the way and start a new game.+ -- If the savefile was randomly corrupted or made read-only,+ -- that should solve the problem. Serious IO problems (e.g. failure+ -- to create a user data directory) terminate the program with an exception.+ res <- Ex.try $+ if sb+ then do+ (s, cli) <- strictDecodeEOF saveFileBkp+ let msg = "Welcome back to" <+> title <> "."+ return $ Left (s, cli, msg)+ else+ if bb+ then do+ (s, cli) <- strictDecodeEOF saveFileBkp+ let msg = "No client savefile found."+ <+> "Restoring from a backup savefile."+ return $ Left (s, cli, msg)+ else do+ let msg = "Welcome to" <+> title <> "!"+ return $ Right msg+ let handler :: Ex.SomeException -> IO (Either (State, StateClient, Msg) Msg)+ handler e = let msg = "Client restore failed. The error message is:"+ <+> (T.unwords . T.lines) (showT e)+ in return $ Right msg+ either handler return res
+ Game/LambdaHack/Client/AtomicSemCli.hs view
@@ -0,0 +1,661 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Semantics of client UI response to atomic commands.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Client.AtomicSemCli+ ( cmdAtomicSem, cmdAtomicSemCli, cmdAtomicFilterCli+ , drawCmdAtomicUI, drawSfxAtomicUI+ ) where++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 qualified NLP.Miniutter.English as MU++import Game.LambdaHack.Client.Action+import Game.LambdaHack.Client.Draw+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.Animation+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.AtomicPos+import Game.LambdaHack.Common.AtomicSem+import qualified Game.LambdaHack.Common.Color as Color+import qualified Game.LambdaHack.Common.Effect as Effect+import Game.LambdaHack.Common.Faction+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.Point+import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Utils.Assert++-- * CmdAtomicAI++-- | Clients keep a subset of atomic commands sent by the server+-- and add some of their own. The result of this function is the list+-- of commands kept for each command received.+cmdAtomicFilterCli :: MonadClient m => CmdAtomic -> m [CmdAtomic]+cmdAtomicFilterCli cmd = case cmd of+ AlterTileA lid p fromTile toTile -> do+ Kind.COps{cotile = Kind.Ops{oname}} <- getsState scops+ lxsize <- getsLevel lid lxsize+ t <- getsLevel lid (`at` p)+ if t == fromTile+ then return [cmd]+ else do+ -- From alterTileA@ we know @t == freshClientTile@,+ -- which is uncanny, so we produce a message.+ -- It happens when a client thinks the tile is @t@,+ -- but it's @fromTile@, and @AlterTileA@ changes it+ -- to @toTile@. See @alterTileA@.+ let subject = "" -- a hack, we we don't handle adverbs well+ verb = "turn into"+ msg = makeSentence [ "the", MU.Text $ oname t+ , "at position", MU.Text $ showPoint lxsize p+ , "suddenly" -- adverb+ , MU.SubjectVerbSg subject verb+ , MU.AW $ MU.Text $ oname toTile ]+ return [ cmd -- reveal the tile+ , MsgAllA msg -- show the message+ ]+ SearchTileA aid p fromTile toTile -> do+ b <- getsState $ getActorBody aid+ t <- getsLevel (blid b) (`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+ ]+ else -- Misguided. Should never happen, LoseTile resets memory.+ assert `failure` (t, cmd)+ DiscoverA _ _ iid _ -> do+ disco <- getsClient sdisco+ item <- getsState $ getItemBody iid+ if jkindIx item `EM.member` disco+ then return []+ else return [cmd]+ CoverA _ _ iid _ -> do+ disco <- getsClient sdisco+ item <- getsState $ getItemBody iid+ if jkindIx item `EM.notMember` disco+ then return []+ else return [cmd]+ PerceptionA lid outPA inPA -> 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+ perNew <- getPerFid lid+ s <- getState+ fid <- getsClient sside+ -- Wipe out actors that just became invisible due to changed FOV.+ -- TODO: perhaps instead create LoseActorA for all actors in lprio,+ -- 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)+ outActor = mapMaybe fActor outPrio+ -- Wipe out remembered items on tiles that now came into view.+ lfloor <- getsLevel lid lfloor+ let inFov = totalVisible perNew ES.\\ totalVisible perOld+ pMaybe p = maybe Nothing (\x -> Just (p, x))+ inFloor = mapMaybe (\p -> pMaybe p $ EM.lookup p lfloor)+ (ES.elems inFov)+ fItem p (iid, k) = LoseItemA iid (getItemBody iid s) k (CFloor lid p)+ fBag (p, bag) = map (fItem p) $ EM.assocs bag+ inItem = concatMap fBag inFloor+ -- Remembered map tiles not wiped out, due to optimization in @spotTileA@.+ -- Wipe out remembered smell on tiles that now came into smell Fov.+ lsmell <- getsLevel lid lsmell+ let inSmellFov = smellVisible perNew ES.\\ smellVisible perOld+ inSm = mapMaybe (\p -> pMaybe p $ EM.lookup p lsmell)+ (ES.elems inSmellFov)+ inSmell = if null inSm then [] else [LoseSmellA lid inSm]+ let seenNew = seenAtomicCli False fid perNew+ seenOld = seenAtomicCli False fid perOld+ -- TODO: these assertions are probably expensive+ psActor <- mapM posCmdAtomic outActor+ -- Verify that we forget only previously seen actors.+ assert (allB seenOld psActor) skip+ -- Verify that we forget only currently invisible actors.+ assert (allB (not . seenNew) psActor) skip+ psItemSmell <- mapM posCmdAtomic $ inItem ++ inSmell+ -- Verify that we forget only previously invisible items and smell.+ 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]++-- | 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+ LeadFactionA fid source target -> do+ side <- getsClient sside+ when (side == fid) $ do+ mleader <- getsClient _sleader+ assert (mleader == source -- somebody changed the leader for us+ || mleader == target -- we changed the leader originally+ `blame` (cmd, mleader)) skip+ 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+ 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}+ ResumeA _fid sfper -> modifyClient $ \cli -> cli {sfper}+ KillExitA _fid -> killExitA+ SaveExitA -> saveExitA+ SaveBkpA -> clientGameSave True+ _ -> return ()++perceptionA :: MonadClient m => LevelId -> PerActor -> PerActor -> m ()+perceptionA lid outPA inPA = do+ cops <- getsState scops+ s <- getState+ -- 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.+ -- 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))+ 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` lid+ adj (Just per) = Just $ dummyToPer $ addPer (diffPer per outPer) inPer+ f = EM.alter adj lid+ modifyClient $ \cli -> cli {sfper = f (sfper cli)}++discoverA :: MonadClient m+ => LevelId -> Point -> ItemId -> Kind.Id ItemKind -> m ()+discoverA lid p iid ik = do+ item <- getsState $ getItemBody iid+ let f Nothing = Just ik+ f (Just ik2) = assert `failure` (lid, p, iid, ik, ik2)+ modifyClient $ \cli -> cli {sdisco = EM.alter f (jkindIx item) (sdisco cli)}++coverA :: MonadClient m+ => LevelId -> Point -> ItemId -> Kind.Id ItemKind -> m ()+coverA lid p iid ik = do+ item <- getsState $ getItemBody iid+ let f Nothing = assert `failure` (lid, p, iid, ik)+ f (Just ik2) = assert (ik == ik2 `blame` (ik, ik2)) Nothing+ modifyClient $ \cli -> cli {sdisco = EM.alter f (jkindIx item) (sdisco cli)}++killExitA :: MonadClient m => m ()+killExitA = modifyClient $ \cli -> cli {squit = True}++saveExitA :: MonadClient m => m ()+saveExitA = do+ clientGameSave False+ modifyClient $ \cli -> cli {squit = True}++-- * CmdAtomicUI++-- TODO: let user configure which messages are not created, which are+-- slightly hidden, which are shown and which flash and center screen+-- and perhaps highligh the related location/actor. Perhaps even+-- switch to the actor, changing HP displayed on screen, etc.+-- but it's too short a clip to read the numbers, so probably+-- highlighing should be enough.+-- TODO: for a start, flesh out the verbose variant and then add+-- a single client debug option that flips verbosity+--+-- | Visualization of atomic actions for the client is perfomed+-- in the global state after the command is executed and after+-- 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 _ -> do+ side <- getsClient sside+ if bhp body <= 0 && not (bproj body) && bfid body == side then do+ actorVerbMU aid body "die"+ void $ displayMore ColorBW ""+ else when verbose $ actorVerbMU aid body "disappear"+ CreateItemA _ item k _ | verbose -> itemVerbMU item k "appear"+ DestroyItemA _ item k _ | verbose -> itemVerbMU item k "disappear"+ LoseActorA aid body _ -> do+ side <- getsClient sside+ -- If no other faction actor is looking, death is invisible and+ -- so is domination, time-freeze, etc. Then, this command appears instead.+ when (bfid body == side && bhp body <= 0 && not (bproj body)) $ do+ actorVerbMU aid body "be missing in action"+ void $ displayMore ColorFull ""+ 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"+ HasteActorA aid delta ->+ aVerbMU aid $ if delta > speedZero+ then "speed up"+ else "slow down"+ LeadFactionA fid (Just source) (Just target) -> do+ Kind.COps{coactor} <- getsState scops+ side <- getsClient sside+ when (fid == side) $ do+ actorD <- getsState sactorD+ case EM.lookup source actorD of+ Just sb | bhp sb <= 0 -> assert (not $ bproj sb) $ do+ -- Regardless who the leader is, give proper names here, not 'you'.+ tb <- getsState $ getActorBody target+ let subject = partActor coactor tb+ object = partActor coactor sb+ msgAdd $ makeSentence [ MU.SubjectVerbSg subject "take command"+ , "from", object ]+ _ -> skip+ DiplFactionA fid1 fid2 _ toDipl -> do+ name1 <- getsState $ gname . (EM.! fid1) . sfactionD+ name2 <- getsState $ gname . (EM.! fid2) . sfactionD+ let showDipl Unknown = "unknown to each other"+ showDipl Neutral = "in neutral diplomatic relations"+ showDipl Alliance = "allied"+ showDipl War = "at war"+ msgAdd $ name1 <+> "and" <+> name2 <+> "are now" <+> showDipl toDipl <> "."+ QuitFactionA fid mbody _ toSt -> quitFactionUI fid mbody toSt+ AlterTileA{} | verbose ->+ return () -- TODO: door opens+ SearchTileA aid _ fromTile toTile -> do+ Kind.COps{cotile = Kind.Ops{oname}} <- getsState scops+ subject <- partAidLeader aid+ let verb = "reveal that the"+ subject2 = MU.Text $ oname fromTile+ verb2 = "be"+ let msg = makeSentence [ MU.SubjectVerbSg subject verb+ , MU.SubjectVerbSg subject2 verb2+ , "a hidden"+ , MU.Text $ oname toTile ]+ msgAdd msg+ AgeGameA t -> do+ when (t > timeClip) $ displayFrames [Nothing] -- show delay+ -- TODO: shows messages on leader level, instead of recently shown+ -- level (e.g., between animations); perhaps draw messages separately+ -- from level (but on the same text window) or keep last level frame+ -- and only overlay messages on it when needed; or store the level+ -- of last shown+ displayPush -- TODO: is this really needed? write why+ DiscoverA _ _ iid _ -> do+ disco <- getsClient sdisco+ item <- getsState $ getItemBody iid+ let ix = jkindIx item+ Kind.COps{coitem} <- getsState scops+ let discoUnknown = EM.delete ix disco+ (objUnkown1, objUnkown2) = partItem coitem discoUnknown item+ msg = makeSentence+ [ "the", MU.SubjectVerbSg (MU.Phrase [objUnkown1, objUnkown2])+ "turn out to be"+ , partItemAW coitem disco item ]+ msgAdd msg+ CoverA _ _ iid ik -> do+ discoUnknown <- getsClient sdisco+ item <- getsState $ getItemBody iid+ let ix = jkindIx item+ Kind.COps{coitem} <- getsState scops+ let disco = EM.insert ix ik discoUnknown+ (objUnkown1, objUnkown2) = partItem coitem discoUnknown item+ (obj1, obj2) = partItem coitem disco item+ msg = makeSentence+ [ "the", MU.SubjectVerbSg (MU.Phrase [obj1, obj2])+ "look like an ordinary"+ , objUnkown1, objUnkown2 ]+ msgAdd msg+ RestartA _ _ _ _ _ t ->+ msgAdd $ "New game started in" <+> t <+> "mode."+ SaveBkpA | verbose -> msgAdd "Saving backup."+ MsgAllA msg -> msgAdd msg+ _ -> return ()++lookAtMove :: MonadClientUI m => ActorId -> m ()+lookAtMove aid = do+ body <- getsState $ getActorBody aid+ side <- getsClient sside+ tgtMode <- getsClient stgtMode+ when (not (bproj body)+ && bfid body == side+ && isNothing tgtMode) $ do -- targeting does a more extensive look+ lookMsg <- lookAt False True (bpos body) aid ""+ msgAdd lookMsg++-- | Sentences such as \"Dog barks loudly.\".+actorVerbMU :: MonadClientUI m => ActorId -> Actor -> MU.Part -> m ()+actorVerbMU aid b verb = do+ subject <- partActorLeader aid b+ msgAdd $ makeSentence [MU.SubjectVerbSg subject verb]++aVerbMU :: MonadClientUI m => ActorId -> MU.Part -> m ()+aVerbMU aid verb = do+ b <- getsState $ getActorBody aid+ actorVerbMU aid b verb++itemVerbMU :: MonadClientUI m => Item -> Int -> MU.Part -> m ()+itemVerbMU item k verb = do+ Kind.COps{coitem} <- getsState scops+ disco <- getsClient sdisco+ let msg =+ makeSentence [MU.SubjectVerbSg (partItemWs coitem disco k item) verb]+ msgAdd msg++_iVerbMU :: MonadClientUI m => ItemId -> Int -> MU.Part -> m ()+_iVerbMU iid k verb = do+ item <- getsState $ getItemBody iid+ itemVerbMU item k verb++aiVerbMU :: MonadClientUI m => ActorId -> MU.Part -> ItemId -> Int -> m ()+aiVerbMU aid verb iid k = do+ Kind.COps{coitem} <- getsState scops+ disco <- getsClient sdisco+ item <- getsState $ getItemBody iid+ subject <- partAidLeader aid+ let msg = makeSentence [ MU.SubjectVerbSg subject verb+ , partItemWs coitem disco k item ]+ msgAdd msg++moveItemUI :: MonadClientUI m+ => Bool -> ItemId -> Int -> Container -> Container -> m ()+moveItemUI verbose iid k c1 c2 = do+ Kind.COps{coitem} <- getsState scops+ item <- getsState $ getItemBody iid+ disco <- getsClient sdisco+ case (c1, c2) of+ (CFloor _ _, CActor aid l) -> do+ b <- getsState $ getActorBody aid+ unless (bproj b) $ do+ let n = bbag b EM.! iid+ side <- getsClient sside+ if bfid b == side then+ msgAdd $ makePhrase [ letterLabel l+ , partItemWs coitem disco n item+ , "\n" ]+ else aiVerbMU aid "pick up" iid k+ (CActor aid _, CFloor _ _) | verbose ->+ aiVerbMU aid "drop" iid k+ _ -> return ()++displaceActorUI :: MonadClientUI m => ActorId -> ActorId -> m ()+displaceActorUI source target = do+ sb <- getsState $ getActorBody source+ tb <- getsState $ getActorBody target+ spart <- partActorLeader source sb+ tpart <- partActorLeader target tb+ let msg = makeSentence [MU.SubjectVerbSg spart "displace", tpart]+ msgAdd msg+ when (bfid sb /= bfid tb) $ do+ lookAtMove source+ lookAtMove target+ let ps = (bpos tb, bpos sb)+ animFrs <- animate (blid sb) $ swapPlaces ps+ displayFrames $ Nothing : animFrs++quitFactionUI :: MonadClientUI m+ => FactionId -> Maybe Actor -> Maybe Status -> m ()+quitFactionUI fid mbody toSt = do+ Kind.COps{coitem=Kind.Ops{oname, ouniqGroup}} <- getsState scops+ factionD <- getsState sfactionD+ let fact = factionD EM.! fid+ fidName = MU.Text $ gname 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.+ Just Status{stOutcome=Killed} ->+ ( Just "be eliminated"+ , msgIfSide "Let's hope another party can save the day!" )+ Just Status{stOutcome=Defeated} ->+ ( Just "be decisively defeated"+ , msgIfSide "Let's hope your new overlords let you live." )+ Just Status{stOutcome=Camping} ->+ ( Just "order save and exit"+ , Just $ if fid == side+ then "See you soon, stronger and braver!"+ else "See you soon, stalwart warrior!" )+ Just Status{stOutcome=Conquer} ->+ ( Just "vanquish all foes"+ , msgIfSide "Can it be done in a better style, though?" )+ Just Status{stOutcome=Escape} ->+ ( Just "achieve victory"+ , msgIfSide "Can it be done better, though?" )+ Just Status{stOutcome=Restart, stInfo} ->+ ( Just $ MU.Text $ "order mission restart in" <+> stInfo <+> "mode"+ , Just $ if fid == side+ then "This time for real."+ else "Somebody couldn't stand the heat." )+ Nothing ->+ (Nothing, Nothing) -- Wipe out the quit flag for the savegame files.+ case startingPart of+ Nothing -> return ()+ Just sp -> do+ let msg = makeSentence [MU.SubjectVerbSg fidName sp]+ msgAdd msg+ case (toSt, partingPart) of+ (Just status, Just pp) -> do+ (bag, total) <- case mbody of+ Just body | fid == side -> getsState $ calculateTotal body+ _ -> case gleader fact of+ Nothing -> return (EM.empty, 0)+ Just aid -> do+ b <- getsState $ getActorBody aid+ getsState $ calculateTotal b+ let currencyName = MU.Text $ oname $ ouniqGroup "currency"+ itemMsg = makeSentence [ "Your loot is worth"+ , MU.CarWs total currencyName ]+ <+> moreMsg+ startingSlide <- promptToSlideshow moreMsg+ recordHistory -- we are going to exit or restart, so record+ itemSlides <-+ if EM.null bag then return Monoid.mempty+ else do+ io <- floorItemOverlay bag+ overlayToSlideshow itemMsg io+ scoreSlides <- scoreToSlideshow total status+ partingSlide <- promptToSlideshow $ pp <+> moreMsg+ shutdownSlide <- promptToSlideshow pp+ -- TODO: First ESC cancels items display.+ void $ getInitConfirms []+ $ startingSlide Monoid.<> 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+ _ -> return ()++-- * SfxAtomicUI++drawSfxAtomicUI :: MonadClientUI m => Bool -> SfxAtomic -> m ()+drawSfxAtomicUI verbose sfx = case sfx of+ StrikeD source target item b -> strikeD source target item b+ RecoilD source target _ _ -> do+ spart <- partAidLeader source+ tpart <- partAidLeader target+ msgAdd $ makeSentence [MU.SubjectVerbSg spart "shrink back from", tpart]+ ProjectD aid iid -> aiVerbMU aid "aim" iid 1+ CatchD aid iid -> aiVerbMU aid "catch" iid 1+ ActivateD aid iid -> aiVerbMU aid "activate"{-TODO-} iid 1+ CheckD aid iid -> aiVerbMU aid "check" iid 1+ TriggerD aid _p _feat _ | verbose ->+ aVerbMU aid "trigger" -- TODO: opens door+ ShunD aid _p _ _ | verbose ->+ aVerbMU aid "shun" -- TODO: shuns stairs down+ EffectD aid effect -> do+ b <- getsState $ getActorBody aid+ side <- getsClient sside+ let fid = bfid b+ if bhp b <= 0 && not (bproj b) || bhp b < 0 then do+ -- We assume the effect is the cause of incapacitation.+ let firstFall | fid == side && bproj b = "fall apart"+ | fid == side = "fall down"+ | bproj b = "break up"+ | otherwise = "collapse"+ hurtExtra | fid == side && bproj b = "be stomped flat"+ | fid == side = "be ground into the floor"+ | bproj b = "be shattered into little pieces"+ | otherwise = "be reduced to a bloody pulp"+ subject <- partActorLeader aid b+ let deadPreviousTurn p = p < 0+ && (bhp b <= p && not (bproj b)+ || bhp b < p)+ (deadBefore, verbDie) =+ case effect of+ Effect.Hurt _ p | deadPreviousTurn p -> (True, hurtExtra)+ Effect.Heal p | deadPreviousTurn p -> (True, hurtExtra)+ _ -> (False, firstFall)+ msgDie = makeSentence [MU.SubjectVerbSg subject verbDie]+ msgAdd msgDie+ when (fid == side && not (bproj b)) $ do+ animDie <- if deadBefore+ then animate (blid b)+ $ twirlSplash (bpos b, bpos b) Color.Red Color.Red+ else animate (blid b) $ deathBody $ bpos b+ displayFrames animDie+ else case effect of+ Effect.NoEffect -> msgAdd "Nothing happens."+ Effect.Heal p | p > 0 -> do+ if fid == side then+ actorVerbMU aid b "feel healthier"+ else+ actorVerbMU aid b "look healthier"+ let ps = (bpos b, bpos b)+ animFrs <- animate (blid b) $ twirlSplash ps Color.BrBlue Color.Blue+ displayFrames $ Nothing : animFrs+ Effect.Heal _ -> do+ if fid == side then+ actorVerbMU aid b "feel wounded"+ else+ actorVerbMU aid b "look wounded"+ let ps = (bpos b, bpos b)+ animFrs <- animate (blid b) $ twirlSplash ps Color.BrRed Color.Red+ displayFrames $ Nothing : animFrs+ Effect.Mindprobe nEnemy -> do+ let msg = makeSentence+ [MU.CardinalWs nEnemy "howl", "of anger", "can be heard"]+ msgAdd msg+ Effect.Dominate ->+ if fid == side then lookAtMove aid+ else do+ fidName <- getsState $ gname . (EM.! fid) . sfactionD+ aVerbMU aid $ MU.Text $ "fall under the influence of" <+> fidName+ Effect.ApplyPerfume ->+ msgAdd "The fragrance quells all scents in the vicinity."+ Effect.Searching{} -> do+ subject <- partActorLeader aid b+ let msg = makeSentence+ [ "It gets lost and"+ , MU.SubjectVerbSg subject "search in vain" ]+ msgAdd msg+ Effect.Ascend{} -> actorVerbMU aid b "find a way upstairs"+ Effect.Descend{} -> actorVerbMU aid b "find a way downstairs"+ _ -> return ()+ MsgFidD _ msg -> msgAdd msg+ MsgAllD msg -> msgAdd msg+ DisplayPushD _ ->+ -- TODO: shows messages on leader level, instead of recently shown+ -- level (e.g., between animations); perhaps draw messages separately+ -- from level (but on the same text window) or keep last level frame+ -- and only overlay messages on it when needed; or store the level+ -- of last shown+ displayPush+ DisplayDelayD _ -> displayFrames [Nothing]+ _ -> return ()++strikeD :: MonadClientUI m+ => ActorId -> ActorId -> Item -> HitAtomic -> m ()+strikeD source target item b = assert (source /= target) $ do+ Kind.COps{coitem=coitem@Kind.Ops{okind}} <- getsState scops+ disco <- getsClient sdisco+ sb <- getsState $ getActorBody source+ tb <- getsState $ getActorBody target+ spart <- partActorLeader source sb+ tpart <- partActorLeader target tb+ let (verb, withWhat) | bproj sb = ("hit", False)+ | otherwise =+ case jkind disco item of+ Nothing -> ("hit", False) -- not identified+ Just ik -> let kind = okind ik+ in ( iverbApply kind+ , isNothing $ lookup "hth" $ ifreq kind )+ msg MissBlockD =+ let (partBlock1, partBlock2) =+ if withWhat+ then ("swing", partItemAW coitem disco item)+ else ("try to", verb)+ in makeSentence+ [ MU.SubjectVerbSg spart partBlock1+ , partBlock2 MU.:> ", but"+ , MU.SubjectVerbSg tpart "block"+ ]+ msg _ = makeSentence $+ [MU.SubjectVerbSg spart verb, tpart]+ ++ if withWhat+ then ["with", partItemAW coitem disco item]+ else []+ msgAdd $ msg b+ let ps = (bpos tb, bpos sb)+ anim HitD = twirlSplash ps Color.BrRed Color.Red+ anim HitBlockD = blockHit ps Color.BrRed Color.Red+ anim MissBlockD = blockMiss ps+ animFrs <- animate (blid sb) $ anim b+ displayFrames $ Nothing : animFrs
+ Game/LambdaHack/Client/Binding.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Generic binding of keys to commands, procesing macros,+-- printing command help. No operation in this module+-- involves the 'State' or 'Action' type.+module Game.LambdaHack.Client.Binding+ ( Binding(..), stdBinding, keyHelp,+ ) where++import Control.Arrow (second)+import qualified Data.Char as Char+import qualified Data.List as L+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 qualified Game.LambdaHack.Common.Key as K+import Game.LambdaHack.Common.Msg++-- | 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+ }++-- | 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+ heroSelect k = ( K.KM { key=K.Char (Char.intToDigit k)+ , modifier=K.NoModifier }+ , SelectHero k )+ cmdList =+ configCommands config+ ++ K.moveBinding Move Run+ ++ fmap heroSelect [0..9]+ mkDescribed cmd = (cmdDescription cmd, noRemoteHumanCmd cmd, cmd)+ mkCommand = second mkDescribed+ semList = L.map mkCommand cmdList+ 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+ }++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} =+ let+ movBlurb =+ [ "Move throughout the level with 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"+ , ""+ ,"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."+ ]+ majorBlurb =+ [ ""+ , "Commands marked with * take time and are blocked on remote levels."+ , "Press SPACE to see the next page, with the list of minor commands."+ ]+ minorBlurb =+ [ ""+ , "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+ 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+ ]
+ Game/LambdaHack/Client/ClientSem.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Semantics of most 'CmdClientAI' client commands.+module Game.LambdaHack.Client.ClientSem where++import Control.Monad+import Control.Monad.Writer.Strict (WriterT, runWriterT)+import qualified Data.EnumMap.Strict as EM+import qualified Data.Map.Strict as M+import Data.Maybe+import qualified Data.Text as T++import Game.LambdaHack.Client.Action+import Game.LambdaHack.Client.Binding+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 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.Point+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Common.ServerCmd+import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.StrategyKind+import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.Frequency++queryAI :: MonadClient m => ActorId -> m CmdSer+queryAI oldAid = do+ Kind.COps{costrat=Kind.Ops{okind}} <- getsState scops+ side <- getsClient sside+ fact <- getsState $ \s -> sfactionD s EM.! side+ let aiMember = fmap okind $ gAiMember fact+ mleader <- getsClient _sleader+ if -- Keep the leader: only a leader is allowed to pick another leader.+ mleader /= Just oldAid+ -- Keep the leader: AIs are the same.+ || gAiLeader fact == gAiMember fact+ -- Keep the leader: other members can't melee.+ || Ability.Melee `notElem` maybe [] sabilities aiMember+ then queryAIPick oldAid+ else do+ fper <- getsClient sfper+ visFoes <- visibleFoes fper oldAid+ oldBody <- getsState $ getActorBody oldAid+ btarget <- getsClient $ getTarget oldAid+ let arena = blid oldBody+ ours <- getsState $ actorNotProjAssocs (== side) arena+ Level{lxsize} <- getsState $ \s -> sdungeon s EM.! arena+ if -- Keep the leader: he is alone on the level.+ length ours == 1+ -- Keep the leader: it still has an enemy target (even if not visible)+ || case btarget of Just TEnemy{} -> True; _ -> False+ -- ... and he is not yet adjacent to any foe.+ && all (not . adjacent lxsize (bpos oldBody))+ (map (bpos . snd) visFoes)+ then queryAIPick oldAid+ else do+ -- Visibility ignored --- every foe is visible by somebody.+ foes <- getsState $ actorNotProjAssocs (isAtWar fact) arena+ let f (aid, b) =+ let distB = chessDist lxsize (bpos b)+ foeDist = map (\(_, body) -> distB (bpos body)) foes+ minDist | null foeDist = maxBound+ | otherwise = minimum foeDist+ maxChaseDist = 30+ maxProximity = max 1 $ maxChaseDist - minDist+ in if aid == oldAid || minDist == 1+ then Nothing -- ignore, leader or already in melee range+ else Just (maxProximity, aid)+ candidates = mapMaybe f ours+ freq | null candidates = toFreq "old leader" [(1, oldAid)]+ | otherwise = toFreq "candidates for AI leader" candidates+ aid <- rndToAction $ frequency freq+ s <- getState+ modifyClient $ updateLeader aid s+ queryAIPick aid++queryAIPick :: MonadClient m => ActorId -> m CmdSer+queryAIPick aid = do+ side <- getsClient sside+ body <- getsState $ getActorBody aid+ assert (bfid body == side `blame` (aid, bfid body, side)) skip+ Kind.COps{costrat=Kind.Ops{okind}} <- getsState scops+ leader <- getsClient _sleader+ fact <- getsState $ (EM.! bfid body) . sfactionD+ let factionAI | Just aid /= leader = fromJust $ gAiMember fact+ | otherwise = fromJust $ gAiLeader fact+ factionAbilities = sabilities (okind factionAI)+ 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+ -- 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++-- | Handle the move of the hero.+queryUI :: (MonadClientAbort m, 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.+ leader <- getLeaderUI+ assert (leader == aid `blame` (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)}+ -- Attacks and opening doors disallowed when continuing to run.+ return $ RunSer leader dir++-- | 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+ -> m CmdSer+humanCommand msgRunAbort = do+ let loop :: Overlay -> m CmdSer+ loop overlay = do+ km <- getKeyOverlayCommand overlay+ -- 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+ -- Look up the key.+ Binding{kcmd} <- askBinding+ case M.lookup km kcmd 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+ Nothing -> let msgKey = "unknown command <" <> K.showKM km <> ">"+ in abortWith msgKey+ -- The command was aborted or successful and if the latter,+ -- possibly took some time.+ case mcmdS of+ Just cmdS -> assert (null (runSlideshow slides) `blame` slides) $ 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})+ return cmdS+ Nothing -> do+ -- If no time taken, rinse and repeat.+ -- Analyse the obtained slides.+ mLast <- case reverse (runSlideshow slides) of+ [] -> return Nothing+ [sLast] -> return $ Just 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.+ b <- getInitConfirms [km] $ toSlideshow $ reverse sls+ return $! if b 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
+ Game/LambdaHack/Client/Config.hs view
@@ -0,0 +1,45 @@+-- | Personal game configuration file type definitions.+module Game.LambdaHack.Client.Config+ ( ConfigUI(..)+ ) where++import Control.DeepSeq+import Data.Binary++import Game.LambdaHack.Client.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+ , configAppDataDirUI :: !FilePath+ , configUICfgFile :: !FilePath+ -- macros+ , configMacros :: ![(K.KM, K.KM)]+ -- ui+ , configFont :: !String+ , configHistoryMax :: !Int+ }+ deriving Show++instance NFData ConfigUI++instance Binary ConfigUI where+ put ConfigUI{..} = do+ put configCommands+ put configAppDataDirUI+ put configUICfgFile+ put configMacros+ put configFont+ put configHistoryMax+ get = do+ configCommands <- get+ configAppDataDirUI <- get+ configUICfgFile <- get+ configMacros <- get+ configFont <- get+ configHistoryMax <- get+ return ConfigUI{..}
+ Game/LambdaHack/Client/Draw.hs view
@@ -0,0 +1,198 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Display game data on the screen using one of the available frontends+-- (determined at compile time with cabal flags).+module Game.LambdaHack.Client.Draw+ ( ColorMode(..), draw+ ) where++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 Game.LambdaHack.Client.State+import Game.LambdaHack.Common.Actor as Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.Animation (SingleFrame (..))+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.Item+import qualified Game.LambdaHack.Common.Item as 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.Point+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.TileKind++-- | Color mode for the display.+data ColorMode =+ ColorFull -- ^ normal, with full colours+ | 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+ -> SingleFrame+draw dm cops per drawnLevelId mleader+ StateClient{ stgtMode, scursor, seps, sdisco+ , smarkVision, smarkSmell, smarkSuspect }+ s overlay =+ let Kind.COps{ coactor=Kind.Ops{okind}+ , 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+ (Just cursor, Just leader) ->+ let Actor{bpos, blid} = getActorBody leader s+ in if blid /= drawnLevelId+ then []+ else fromMaybe [] $ bla lxsize lysize seps bpos cursor+ _ -> []+ dis pxy =+ let pos0 = toPoint lxsize pxy+ 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{bkind, bsymbol, bcolor, bhp, bproj}+ | Just aid == mleader = (symbol, Color.defBG)+ | otherwise = (symbol, color)+ where+ ActorKind{asymbol, acolor} = okind bkind+ color = fromMaybe acolor bcolor+ symbol | bhp <= 0 && not bproj = '%'+ | otherwise = fromMaybe asymbol bsymbol+ rainbow p = toEnum $ fromEnum p `rem` 14 + 1+ actorsHere = actorAssocs (const True) drawnLevelId s+ -- 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+ _ | smarkSmell && smlt > timeZero ->+ (timeToDigit smellTimeout smlt, rainbow pos0)+ | otherwise ->+ case EM.keys items of+ [] -> (tsymbol tk, vcolor tk)+ i : _ -> Item.viewItem $ getItemBody i s+ vis = ES.member pos0 $ totalVisible per+ visPl =+ maybe False (\leader -> actorSeesLoc 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+ 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 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.+ 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 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:" <+> "--"+ <+> "(" <> "--" <> ")")
+ Game/LambdaHack/Client/HumanCmd.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}+-- | 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 qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.VectorXY+import Game.LambdaHack.Utils.Assert++-- | Abstract syntax of player commands.+data HumanCmd =+ -- These usually take time.+ Move VectorXY+ | Run VectorXY+ | Wait+ | Pickup+ | Drop+ | Project [Trigger]+ | Apply [Trigger]+ | TriggerDir [Trigger]+ | TriggerTile [Trigger]+ -- These do not take time.+ | GameRestart Text+ | GameExit+ | GameSave+ | CfgDump+ -- 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}+ | BumpFeature {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+ TriggerDir{} -> 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+-- CfgDump -> True+ 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+ TriggerDir{} -> 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+ TriggerDir ts -> triggerDescription ts+ TriggerTile ts -> triggerDescription ts++ GameRestart t -> "new" <+> t <+> "game"+ GameExit -> "save and exit"+ GameSave -> "save game"+ CfgDump -> "dump current configuration"++ 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 in targeting in config file" :: Text)+ 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
@@ -0,0 +1,432 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}+-- | Semantics of 'Command.Cmd' client commands that return server commands.+-- A couple of them do not take time, the rest does.+-- TODO: document+module Game.LambdaHack.Client.HumanGlobal+ ( moveLeader, exploreLeader, runLeader, waitHuman, pickupHuman, dropHuman+ , projectLeader, applyHuman, triggerDirHuman, triggerTileHuman+ , gameRestartHuman, gameExitHuman, gameSaveHuman, cfgDumpHuman+ ) where++import Control.Monad+import qualified Data.EnumMap.Strict as EM+import Data.Function+import Data.List+import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU++import Game.LambdaHack.Client.Action+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 qualified Game.LambdaHack.Common.Effect as Effect+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.Item+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.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.TileKind as TileKind+import Game.LambdaHack.Utils.Assert++-- * Move++moveLeader :: MonadClientUI m => Vector -> m CmdSer+moveLeader dir = do+ leader <- getLeaderUI+ return $! MoveSer leader dir++-- * Explore++exploreLeader :: MonadClientUI m => Vector -> m CmdSer+exploreLeader dir = do+ leader <- getLeaderUI+ return $! ExploreSer leader dir++-- * Run++runLeader :: MonadClientUI m => Vector -> m CmdSer+runLeader dir = do+ leader <- getLeaderUI+ canR <- canRun leader (dir, 0)+ when canR $ modifyClient $ \cli -> cli {srunning = Just (dir, 1)}+ -- Run even if blocked (and then stop), e.g., to open a door.+ return $! RunSer leader dir++-- * Wait++-- | Leader waits a turn (and blocks, etc.).+waitHuman :: MonadClientUI m => m CmdSer+waitHuman = do+ leader <- getLeaderUI+ return $ WaitSer leader++-- * Pickup++pickupHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSer+pickupHuman = do+ leader <- getLeaderUI+ body <- getsState $ getActorBody leader+ lvl <- getsLevel (blid body) id+ -- Check if something is here to pick up. Items are never invisible.+ case EM.minViewWithKey $ lvl `atI` bpos body of+ Nothing -> abortWith "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"++-- * Drop++-- TODO: you can drop an item already on the floor, which works correctly,+-- but is weird and useless.+-- | Drop a single item.+dropHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSer+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++allObjectsName :: Text+allObjectsName = "Objects"++-- | Let the human player choose any item from a list of items.+getAnyItem :: (MonadClientAbort m, 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))+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)+ => ActorId+ -> Text -- ^ prompt message+ -> (Item -> Bool) -- ^ which items to consider suitable+ -> Text -- ^ how to describe suitable items+ -> 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+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ lvl <- getsLevel (blid b) id+ 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+ pos = bpos body+ tis = lvl `atI` pos+ floorFull = not $ EM.null tis+ (floorMsg, floorKey) | floorFull = (", -", [K.Char '-'])+ | otherwise = ("", [])+ isp = filter (p . snd . fst) is0+ bestFull = not $ null isp+ (bestMsg, bestKey)+ | bestFull =+ let bestLetter = invChar $ maximum $ map (snd . snd) isp+ in (", RET(" <> T.singleton bestLetter <> ")", [K.Return])+ | otherwise = ("", [])+ keys ims =+ let mls = map (snd . snd) ims+ ks = bestKey ++ floorKey ++ [K.Char '?']+ ++ map (K.Char . invChar) mls+ in zipWith K.KM (repeat K.NoModifier) ks+ choice ims =+ if null ims+ then "[?" <> floorMsg+ else let mls = map (snd . snd) ims+ r = letterRange mls+ in "[" <> r <> ", ?" <> floorMsg <> bestMsg+ ask = do+ when (null is0 && EM.null tis) $+ abortWith "Not carrying anything."+ perform INone+ invP = EM.filter (\iid -> p (getItemBody iid s)) inv+ perform itemDialogState = do+ let (ims, invOver, msg) = case itemDialogState of+ INone -> (isp, EM.empty, prompt)+ 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 | InvChar l `elem` map (snd . snd) ims ->+ case find ((InvChar l ==) . snd . snd) ims of+ Nothing -> assert `failure` (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` "perform: unexpected key:" <+> K.showKM km+ ask++-- * Project++projectLeader :: (MonadClientAbort m, MonadClientUI m)+ => [Trigger] -> m CmdSer+projectLeader ts = do+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ let lid = blid b+ ms <- getsState $ actorNotProjList (isAtWar fact) lid+ lxsize <- getsLevel lid lxsize+ lysize <- getsLevel lid lysize+ if foesAdjacent lxsize lysize (bpos b) ms+ then abortWith "You can't aim in melee."+ else actorProjectGI leader ts++actorProjectGI :: (MonadClientAbort m, MonadClientUI m)+ => ActorId -> [Trigger] -> m CmdSer+actorProjectGI aid ts = do+ seps <- getsClient seps+ target <- targetToPos+ let (verb1, object1) = case ts of+ [] -> ("aim", "object")+ tr : _ -> (verb tr, object tr)+ triggerSyms = triggerSymbols ts+ case target of+ Just p -> do+ bag <- getsState $ getActorBag aid+ inv <- getsState $ getActorInv aid+ ((iid, _), (_, container)) <-+ getGroupItem aid bag inv object1 triggerSyms+ (makePhrase ["What to", verb1 MU.:> "?"]) "in inventory"+ stgtMode <- getsClient stgtMode+ case stgtMode of+ Just (TgtAuto _) -> endTargeting True+ _ -> return ()+ return $! ProjectSer aid p seps iid container+ Nothing -> assert `failure` (aid, "target unexpectedly invalid")++triggerSymbols :: [Trigger] -> [Char]+triggerSymbols [] = []+triggerSymbols (ApplyItem{..} : ts) = symbol : triggerSymbols ts+triggerSymbols (_ : ts) = triggerSymbols ts++-- * Apply++applyHuman :: (MonadClientAbort m, MonadClientUI m)+ => [Trigger] -> m CmdSer+applyHuman ts = do+ leader <- getLeaderUI+ bag <- getsState $ getActorBag leader+ inv <- getsState $ getActorInv leader+ let (verb1, object1) = case ts of+ [] -> ("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++-- | 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)+ => ActorId+ -> ItemBag -- ^ all objects in question+ -> ItemInv -- ^ inventory characters+ -> MU.Part -- ^ name of the group+ -> [Char] -- ^ accepted item symbols+ -> Text -- ^ prompt+ -> Text -- ^ how to refer to the collection of objects+ -> m ((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)]+ getItem leader prompt choice header is inv packName++-- * TriggerDir++-- | Ask for a direction and trigger a tile, if possible.+triggerDirHuman :: (MonadClientAbort m, MonadClientUI m)+ => [Trigger] -> m CmdSer+triggerDirHuman ts = do+ let verb1 = case ts of+ [] -> "trigger"+ 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+ let dpos dir = bpos b `shift` dir+ lxsize <- getsLevel (blid b) lxsize+ K.handleDir lxsize e (bumpTile leader ts . dpos) (neverMind True)++-- | Player tries to trigger a tile using a feature.+-- To help the player, only visible features can be triggered.+bumpTile :: (MonadClientAbort m, MonadClientUI m)+ => ActorId -> [Trigger] -> Point -> m CmdSer+bumpTile leader ts dpos = do+ Kind.COps{cotile} <- getsState scops+ b <- getsState $ getActorBody leader+ lvl <- getsLevel (blid b) id+ let t = lvl `at` dpos+ triggerFeats = triggerFeatures ts+ -- A tile can be triggered even if an invisible monster occupies it.+ -- TODO: let the user choose whether to attack or activate.+ case filter (\feat -> Tile.hasFeature cotile feat t) triggerFeats of+ [] -> guessBump cotile triggerFeats t+ fs -> do+ mapM_ (verifyTrigger leader) fs+ return $ TriggerSer leader dpos++triggerFeatures :: [Trigger] -> [F.Feature]+triggerFeatures [] = []+triggerFeatures (BumpFeature{..} : ts) = feature : triggerFeatures ts+triggerFeatures (_ : ts) = triggerFeatures ts++-- | Verify important feature triggers, such as fleeing the dungeon.+verifyTrigger :: (MonadClientAbort m, MonadClientUI m)+ => ActorId -> F.Feature -> m ()+verifyTrigger leader feat = case feat of+ F.Cause Effect.Escape -> do+ b <- getsState $ getActorBody leader+ side <- getsClient sside+ spawn <- getsState $ isSpawnFaction side+ summon <- getsState $ isSummonFaction side+ when (spawn || summon) $ abortWith+ "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 ()++-- | Guess and report why the bump command failed.+guessBump :: MonadClientAbort m => Kind.Ops TileKind -> [F.Feature] -> Kind.Id TileKind -> m a+guessBump cotile (F.Openable : _) t | Tile.hasFeature cotile F.Closable t =+ abortWith "already open"+guessBump _ (F.Openable : _) _ =+ abortWith "not a door"+guessBump cotile (F.Closable : _) t | Tile.hasFeature cotile F.Openable t =+ abortWith "already closed"+guessBump _ (F.Closable : _) _ =+ abortWith "not a door"+guessBump cotile (F.Cause (Effect.Ascend _) : _) t+ | Tile.hasFeature cotile F.Descendable t =+ abortWith "the way goes down, not up"+guessBump _ (F.Cause (Effect.Ascend _) : _) _ =+ abortWith "no stairs up"+guessBump cotile (F.Cause (Effect.Descend _) : _) t+ | Tile.hasFeature cotile F.Ascendable t =+ abortWith "the way goes up, not down"+guessBump _ (F.Cause (Effect.Descend _) : _) _ =+ abortWith "no stairs down"+guessBump _ _ _ = neverMind True++-- * TriggerTile++-- | Leader tries to trigger the tile he's standing on.+triggerTileHuman :: (MonadClientAbort m, MonadClientUI m)+ => [Trigger] -> m CmdSer+triggerTileHuman ts = do+ leader <- getLeaderUI+ ppos <- getsState (bpos . getActorBody leader)+ bumpTile leader ts ppos++-- * GameRestart; does not take time++gameRestartHuman :: (MonadClientAbort m, MonadClientUI m) => Text -> m 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++-- * GameExit; does not take time++gameExitHuman :: (MonadClientAbort m, MonadClientUI m) => m CmdSer+gameExitHuman = do+ b <- displayYesNo ColorFull "Really save and exit?"+ if b then do+ leader <- getLeaderUI+ return $ GameExitSer leader+ else abortWith "Save and exit canceled."++-- * GameSave; does not take time++gameSaveHuman :: MonadClientUI m => m CmdSer+gameSaveHuman = do+ leader <- getLeaderUI+ -- TODO: do not save to history:+ msgAdd "Saving game backup."+ return $ GameSaveSer leader++-- * CfgDump; does not take time++cfgDumpHuman :: MonadClientUI m => m CmdSer+cfgDumpHuman = do+ leader <- getLeaderUI+ return $ CfgDumpSer leader
+ Game/LambdaHack/Client/HumanLocal.hs view
@@ -0,0 +1,615 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}+-- | Semantics of 'HumanCmd' client commands that do not return+-- 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+ ) where++-- Cabal+import qualified Paths_LambdaHack as Self (version)++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.Ord+import Data.Text (Text)+import qualified Data.Text as T+import Data.Version+import Game.LambdaHack.Frontend (frontendName)+import qualified NLP.Miniutter.English as MU++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 qualified Game.LambdaHack.Common.Effect as Effect+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.Item+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.State+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Utils.Assert++-- * 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+ let tgtId =+ maybe (assert `failure` "not targetting right now") 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=Kind.Ops{oname}} <- 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."+ if detailed+ then let tile = lvl `at` pos+ in return $! makeSentence [MU.Text $ oname 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+ Kind.COps{coactor} <- getsState scops+ 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 "" (\m -> let subject = partActor coactor m+ verb = "be here"+ in makeSentence [MU.SubjectVerbSg subject verb])+ ihabitant+ vis | not $ p `ES.member` totalVisible per = " (not visible)"+ | actorSeesLoc 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++-- * Project++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++-- * SelectHero++selectHeroHuman :: (MonadClientAbort m, MonadClientUI m) => Int -> m ()+selectHeroHuman k = do+ side <- getsClient sside+ s <- getState+ case tryFindHeroK s side k of+ Nothing -> abortWith "No such member of the party."+ Just (aid, _) -> void $ selectLeader aid++-- * MemberCycle++-- | Switches current member to the next on the level, if any, wrapping.+memberCycleHuman :: (MonadClientAbort m, MonadClientUI m) => m ()+memberCycleHuman = do+ 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."+ (np, b) : _ -> do+ success <- selectLeader np+ assert (success `blame` (leader, np, b)) skip++partyAfterLeader :: MonadActionRO m+ => ActorId+ -> m [(ActorId, Actor)]+partyAfterLeader leader = do+ faction <- getsState $ bfid . getActorBody leader+ allA <- getsState $ EM.assocs . sactorD+ s <- getState+ let hs9 = mapMaybe (tryFindHeroK s faction) [0..9]+ factionA = filter (\(_, body) ->+ not (bproj body) && bfid body == faction) allA+ 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++-- | Select a faction leader. False, if nothing to do.+selectLeader :: MonadClientUI m => ActorId -> m Bool+selectLeader actor = do+ Kind.COps{coactor} <- getsState scops+ leader <- getLeaderUI+ stgtMode <- getsClient stgtMode+ if leader == actor+ then return False -- already selected+ else do+ pbody <- getsState $ getActorBody actor+ -- Even if it's already the leader, give his proper name, not 'you'.+ let subject = partActor coactor pbody+ msgAdd $ makeSentence [subject, "selected"]+ -- Update client state.+ s <- getState+ modifyClient $ updateLeader actor s+ assert (not (bproj pbody) `blame` (actor, pbody)) skip+ -- Move the cursor, if active, to the new level.+ when (isJust stgtMode) $ setTgtId $ blid pbody+ -- Inform about items, etc.+ 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 = do+ leader <- getLeaderUI+ hs <- partyAfterLeader leader+ case reverse hs of+ [] -> abortWith "No other member in the party."+ (np, b) : _ -> do+ success <- selectLeader np+ assert (success `blame` (leader, np, b)) skip++-- * Inventory++-- TODO: When inventory is displayed, let TAB switch the leader (without+-- announcing that) and show the inventory of the new leader.+-- | Display inventory+inventoryHuman :: (MonadClientAbort m, MonadClientUI m)+ => WriterT Slideshow m ()+inventoryHuman = do+ leader <- getLeaderUI+ subject <- partAidLeader leader+ bag <- getsState $ getActorBag leader+ inv <- getsState $ getActorInv leader+ if EM.null bag+ then abortWith $ makeSentence+ [ MU.SubjectVerbSg subject "be"+ , "not carrying anything" ]+ else do+ let blurb = makePhrase+ [MU.Capitalize $ MU.SubjectVerbSg subject "be carrying:"]+ io <- itemOverlay bag inv+ slides <- overlayToSlideshow blurb io+ tell slides++-- * TgtFloor++-- | 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+ 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++-- | 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}+ doLook++-- * TgtEnemy++-- | Start the enemy targeting mode. Cycle between enemy targets.+tgtEnemyLeader :: MonadClientUI m => TgtMode -> WriterT Slideshow m ()+tgtEnemyLeader stgtModeNew = do+ leader <- getLeaderUI+ ppos <- getsState (bpos . getActorBody leader)+ (lid, Level{lxsize}) <- viewedLevel+ per <- getPerFid lid+ target <- getsClient $ getTarget leader+ -- TODO: sort enemies by distance to the leader.+ stgtMode <- getsClient stgtMode+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ ms <- getsState $ actorNotProjAssocs (isAtWar fact) lid+ let plms = filter ((/= leader) . fst) ms -- don't target yourself+ ordPos (_, m) = (chessDist lxsize ppos $ bpos m, bpos m)+ dms = sortBy (comparing ordPos) plms+ (lt, gt) = case target of+ Just (TEnemy n _) | isJust stgtMode -> -- pick next enemy+ let i = fromMaybe (-1) $ findIndex ((== n) . fst) dms+ in splitAt (i + 1) dms+ Just (TEnemy n _) -> -- try to retarget the old enemy+ let i = fromMaybe (-1) $ findIndex ((== n) . fst) dms+ in splitAt i dms+ _ -> (dms, []) -- target first enemy (e.g., number 0)+ gtlt = gt ++ lt+ seen (_, m) =+ let mpos = bpos m -- it is remembered by faction+ in actorSeesLoc 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+ -- Register the chosen enemy, to pick another on next invocation.+ modifyClient $ updateTarget leader (const tgt)+ setCursor stgtModeNew++-- * 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 k = do+ Kind.COps{cotile} <- getsState scops+ dungeon <- getsState sdungeon+ s <- getState+ cursor <- getsClient scursor+ (tgtId, lvl) <- viewedLevel+ let rightStairs = case cursor of+ Nothing -> False+ Just cpos ->+ let tile = lvl `at` cpos+ in k == 1+ && Tile.hasFeature cotile (F.Cause $ Effect.Ascend 1) tile+ || k == -1+ && Tile.hasFeature cotile (F.Cause $ Effect.Descend 1) tile+ if rightStairs -- stairs, in the right direction+ then case whereTo s tgtId k of+ Nothing -> -- we are at the "end" of the dungeon+ abortWith "no more levels in this direction"+ Just (nln, npos) ->+ assert (nln /= tgtId `blame` (nln, "stairs looped")) $ do+ -- 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+ else -- 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)}++-- * EpsIncr++-- | Tweak the @eps@ parameter of the targeting digital line.+epsIncrHuman :: MonadClientAbort m => Bool -> m ()+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++-- * 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 h = do+ stgtMode <- getsClient stgtMode+ if isJust stgtMode+ then endTargeting False+ 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 = case T.uncons t of+ Just ('\n', art) -> map (T.tail . T.init) $ tail . init $ T.lines art+ _ -> assert `failure` "displayMainMenu:" <+> t+ pasteVersion art =+ let pathsVersion = rpathsVersion $ Kind.stdRuleset corule+ version = " Version " ++ showVersion pathsVersion+ ++ " (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.GameSave+ , 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"+ hd : tl -> do+ slides <- overlayToSlideshow hd tl -- TODO: keys don't work if tl/=[]+ tell slides++-- * 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 h = do+ stgtMode <- getsClient stgtMode+ if isJust stgtMode+ then endTargeting True+ 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+ ms <- 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) ms 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 }++endTargetingMsg :: MonadClientUI m => m ()+endTargetingMsg = do+ Kind.COps{coactor} <- getsState scops+ 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 coactor $ getActorBody a s+ else "a fear of the past"+ Just (TPos tpos) ->+ MU.Text $ "position" <+> showPoint lxsize tpos+ Nothing -> "current cursor position continuously"+ 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
+ Game/LambdaHack/Client/HumanSem.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Semantics of human player commands.+module Game.LambdaHack.Client.HumanSem+ ( cmdHumanSem+ ) where++import Control.Monad+import Control.Monad.Writer.Strict (WriterT)+import qualified Data.EnumMap.Strict as EM+import Data.Maybe++import Game.LambdaHack.Client.Action+import Game.LambdaHack.Client.Draw+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.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.ServerCmd+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.Utils.Assert++-- | 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+-- 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)+cmdHumanSem cmd = do+ arena <- getArenaUI+ when (noRemoteHumanCmd cmd) $ checkCursor arena+ cmdAction cmd++-- | The basic action for a command and whether it takes time.+cmdAction :: (MonadClientAbort m, MonadClientUI m)+ => HumanCmd -> WriterT Slideshow m (Maybe CmdSer)+cmdAction cmd = case cmd of+ Move v -> moveHuman v+ Run v -> runHuman v+ Wait -> fmap Just waitHuman+ Pickup -> fmap Just pickupHuman+ Drop -> fmap Just dropHuman+ Project ts -> projectHuman ts+ Apply ts -> fmap Just $ applyHuman ts+ TriggerDir ts -> fmap Just $ triggerDirHuman ts+ TriggerTile ts -> fmap Just $ triggerTileHuman ts++ GameRestart t -> fmap Just $ gameRestartHuman t+ GameExit -> fmap Just gameExitHuman+ GameSave -> fmap Just gameSaveHuman+ CfgDump -> fmap Just cfgDumpHuman++ 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"++moveHuman :: (MonadClientAbort m, MonadClientUI m)+ => VectorXY -> WriterT Slideshow m (Maybe CmdSer)+moveHuman v = do+ Kind.COps{cotile} <- getsState scops+ tgtMode <- getsClient stgtMode+ (arena, lvl@Level{lxsize}) <- viewedLevel+ leader <- getLeaderUI+ sb <- getsState $ getActorBody leader+ if isJust tgtMode then do+ let dir = toDir lxsize v+ moveCursor dir 1 >> return Nothing+ else do+ let dir = toDir lxsize v+ tpos = bpos sb `shift` dir+ t = lvl `at` tpos+ -- We always see actors from our own faction.+ tgt <- getsState $ posToActor tpos arena+ case tgt of+ Just target -> do+ tb <- getsState $ getActorBody target+ sfact <- getsState $ (EM.! bfid sb) . sfactionD+ 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` (leader, target, tb)) skip+ return Nothing+ else do+ 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."+ when (not (bproj tb) && isAllied sfact (bfid tb)) $ do+ go <- displayYesNo ColorBW+ "You are bound by an alliance. Really attack?"+ unless go $ abortWith "Attack canceled."+ fmap Just $ moveLeader dir+ _ -> fmap Just $+ if Tile.hasFeature cotile F.Suspect t+ || Tile.hasFeature cotile F.Openable t+ then -- Explore, because the suspect feature is not yet revealed+ -- or because we want to act on the feature.+ exploreLeader dir+ else -- Don't explore, because the suspect feature is known boring.+ moveLeader dir++runHuman :: MonadClientUI m => VectorXY -> WriterT Slideshow m (Maybe CmdSer)+runHuman v = do+ tgtMode <- getsClient stgtMode+ (_, Level{lxsize}) <- viewedLevel+ if isJust tgtMode then+ let dir = toDir lxsize v+ in moveCursor dir 10 >> return Nothing+ else+ let dir = toDir lxsize v+ in fmap Just $ runLeader dir++projectHuman :: (MonadClientAbort m, MonadClientUI m)+ => [Trigger] -> WriterT Slideshow m (Maybe CmdSer)+projectHuman ts = do+ tgtLoc <- targetToPos+ if isNothing tgtLoc+ then retargetLeader >> return Nothing+ else fmap Just $ projectLeader ts++tgtFloorHuman :: MonadClientUI m => WriterT Slideshow m (Maybe CmdSer)+tgtFloorHuman = do+ arena <- getArenaUI+ tgtFloorLeader (TgtExplicit arena) >> return Nothing++tgtEnemyHuman :: MonadClientUI m => WriterT Slideshow m (Maybe CmdSer)+tgtEnemyHuman = do+ arena <- getArenaUI+ tgtEnemyLeader (TgtExplicit arena) >> return Nothing
+ Game/LambdaHack/Client/LoopAction.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}+-- | The main loop of the client, processing human and computer player+-- moves turn by turn.+module Game.LambdaHack.Client.LoopAction (loopAI, loopUI) where++import Control.Monad+import qualified Data.Text as T++import Game.LambdaHack.Client.Action+import Game.LambdaHack.Client.State+import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.ClientCmd+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.State+import Game.LambdaHack.Utils.Assert++initCli :: MonadClient m => (State -> m ()) -> m (Either Msg Msg)+initCli putSt = do+ -- Warning: state and client state are invalid here, e.g., sdungeon+ -- and sper are empty.+ cops <- getsState scops+ restored <- restoreGame+ case restored of+ Left (s, cli, msg) -> do -- Restore a game or at least history.+ let sCops = updateCOps (const cops) s+ putSt sCops+ putClient cli+ return $ Left msg+ Right msg -> -- First visit ever, use the initial state.+ return $ Right msg++loopAI :: (MonadConnClient CmdClientAI m)+ => (CmdClientAI -> m ()) -> m ()+loopAI cmdClientAISem = do+ side <- getsClient sside+ ConnServer{readConnServer} <- getConn+ msg <- initCli $ \s -> cmdClientAISem $ CmdAtomicAI $ ResumeServerA s+ cmd1 <- readConnServer+ case (msg, cmd1) of+ (Left _, CmdAtomicAI ResumeA{}) -> return ()+ (Left _, CmdAtomicAI RestartA{}) -> return () -- server savefile faulty+ (Right msg1, CmdAtomicAI ResumeA{}) ->+ error $ T.unpack $ "Savefile of client " <> showT side <> " not usable. Can't join the party. Please remove all savefiles manually and restart. Savefile subsystem said: " <> msg1+ (Right _, CmdAtomicAI RestartA{}) -> return ()+ _ -> assert `failure` (side, msg, cmd1)+ cmdClientAISem cmd1+ -- State and client state now valid.+ debugPrint $ "AI client" <+> showT side <+> "started."+ loop+ debugPrint $ "AI client" <+> showT side <+> "stopped."+ where+ loop = do+ ConnServer{readConnServer} <- getConn+ cmd <- readConnServer+ cmdClientAISem cmd+ quit <- getsClient squit+ unless quit loop++loopUI :: (MonadClientUI m, MonadConnClient CmdClientUI m)+ => (CmdClientUI -> m ()) -> m ()+loopUI cmdClientUISem = do+ side <- getsClient sside+ ConnServer{readConnServer} <- getConn+ msg <- initCli $ \s -> cmdClientUISem $ CmdAtomicUI $ ResumeServerA s+ cmd1 <- readConnServer+ case (msg, cmd1) of+ (Left msg1, CmdAtomicUI ResumeA{}) -> do+ cmdClientUISem cmd1+ msgAdd msg1+ (Left _, CmdAtomicUI RestartA{}) -> do+ cmdClientUISem cmd1+ msgAdd "Starting a new game (and ignoring an old client savefile)."+ (Right msg1, CmdAtomicUI ResumeA{}) ->+ error $ T.unpack $ "Savefile of client " <> showT side <> " not usable. Can't join the party. Please remove all savefiles manually and restart. Savefile subsystem said: " <> msg1+ (Right msg1, CmdAtomicUI RestartA{}) -> do+ cmdClientUISem cmd1+ msgAdd msg1+ _ -> assert `failure` (side, msg, cmd1)+ -- State and client state now valid.+ debugPrint $ "UI client" <+> showT side <+> "started."+ loop+ debugPrint $ "UI client" <+> showT side <+> "stopped."+ where+ loop = do+ ConnServer{readConnServer} <- getConn+ cmd <- readConnServer+ cmdClientUISem cmd+ quit <- getsClient squit+ unless quit loop
+ Game/LambdaHack/Client/RunAction.hs view
@@ -0,0 +1,209 @@+-- | Running and disturbance.+module Game.LambdaHack.Client.RunAction+ ( canRun, continueRunDir+ ) where++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.Maybe++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.State+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Utils.Assert++-- | 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 <- getsLevel (blid b) id+ stgtMode <- getsClient stgtMode+ assert (isNothing stgtMode `blame` (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` (pos, dir)+ [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++-- 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) -> X -> Y+ -> (Vector, Int) -> Maybe (Vector, Int)+runDisturbance posLast distLast report hs ms per markSuspect posHere+ posHasFeature posHasItems lxsize lysize (dirNew, distNew) =+ let boringMsgs = map BS.pack [ "Saving backup."+ , "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+ , not . posHasFeature F.Lit+ ]+ -- 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.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 ||+ posHasFeature F.Openable 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++-- | 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+ cops@Kind.COps{cotile} <- getsState scops+ body <- getsState $ getActorBody leader+ let lid = blid body+ per <- getPerFid lid+ sreport <- getsClient sreport -- TODO: check the message before it goes into history+ 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} <- getsLevel (blid body) id+ 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 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.hasFeature cotile F.Openable+ (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
+ Game/LambdaHack/Client/State.hs view
@@ -0,0 +1,206 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Server and client game state types and operations.+module Game.LambdaHack.Client.State+ ( StateClient(..), defStateClient, defHistory+ , updateTarget, getTarget, updateLeader, sside+ , TgtMode(..), Target(..)+ , toggleMarkVision, toggleMarkSmell, toggleMarkSuspect+ ) where++import Control.Monad+import Data.Binary+import qualified Data.EnumMap.Strict as EM+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++import Game.LambdaHack.Client.Config+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Item+import qualified Game.LambdaHack.Common.Key as K+import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Perception+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.State+import Game.LambdaHack.Utils.Assert++-- | Client state, belonging to a single faction.+-- Some of the data, e.g, the history, carries over+-- from game to game, even across playing sessions.+-- Data invariant: if @_sleader@ is @Nothing@ then so is @srunning@.+data StateClient = StateClient+ { stgtMode :: !(Maybe TgtMode) -- ^ targeting mode+ , scursor :: !(Maybe Point) -- ^ cursor coordinates+ , seps :: !Int -- ^ a parameter of the tgt digital line+ , stargetD :: !(EM.EnumMap ActorId Target)+ -- ^ targets of our actors in the dungeon+ , srunning :: !(Maybe (Vector, Int))+ -- ^ direction and distance of running+ , 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)+ , slastKey :: !(Maybe K.KM) -- ^ last command key pressed+ , _sleader :: !(Maybe ActorId) -- ^ selected actor+ , _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+ , sdebugCli :: !Bool -- ^ show client debug messages+ }+ deriving (Show)++-- | 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)++-- | 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+ deriving (Show, Eq)++-- | Initial game client state.+defStateClient :: History -> ConfigUI -> FactionId -> Bool+ -> StateClient+defStateClient shistory sconfigUI _sside sisAI =+ StateClient+ { stgtMode = Nothing+ , scursor = Nothing+ , seps = 0+ , stargetD = EM.empty+ , srunning = Nothing+ , sreport = emptyReport+ , shistory+ , sundo = []+ , sdisco = EM.empty+ , sfper = EM.empty+ , sconfigUI+ , srandom = R.mkStdGen 42 -- will be set later+ , slastKey = Nothing+ , _sleader = Nothing -- no heroes yet alive+ , _sside+ , squit = False+ , sisAI+ , smarkVision = False+ , smarkSmell = False+ , smarkSuspect = False+ , sdebugCli = False+ }++defHistory :: IO History+defHistory = do+ dateTime <- getClockTime+ let curDate = MU.Text $ T.pack $ calendarTimeToString $ toUTCTime dateTime+ 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) }++-- | Get target parameters from client state.+getTarget :: ActorId -> StateClient -> Maybe Target+getTarget aid cli = EM.lookup aid (stargetD cli)++-- | Update selected actor within state. Verify actor's faction.+updateLeader :: ActorId -> State -> StateClient -> StateClient+updateLeader leader s cli =+ let side1 = bfid $ getActorBody leader s+ side2 = sside cli+ in assert (side1 == side2 `blame` (side1, side2, leader, s))+ $ cli {_sleader = Just leader}++sside :: StateClient -> FactionId+sside = _sside++toggleMarkVision :: StateClient -> StateClient+toggleMarkVision s@StateClient{smarkVision} = s {smarkVision = not smarkVision}++toggleMarkSmell :: StateClient -> StateClient+toggleMarkSmell s@StateClient{smarkSmell} = s {smarkSmell = not smarkSmell}++toggleMarkSuspect :: StateClient -> StateClient+toggleMarkSuspect s@StateClient{smarkSuspect} =+ s {smarkSuspect = not smarkSuspect}++instance Binary StateClient where+ put StateClient{..} = do+ put stgtMode+ put scursor+ put seps+ put stargetD+ 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+ get = do+ stgtMode <- get+ scursor <- get+ seps <- get+ stargetD <- 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+ sdebugCli <- get+ let sfper = EM.empty+ srandom = read g+ slastKey = Nothing+ squit = False+ return StateClient{..}++instance Binary TgtMode where+ put (TgtExplicit l) = putWord8 0 >> put l+ put (TgtAuto l) = putWord8 1 >> put l+ get = do+ tag <- getWord8+ case tag of+ 0 -> liftM TgtExplicit get+ 1 -> liftM TgtAuto get+ _ -> fail "no parse (TgtMode)"++instance Binary Target where+ put (TEnemy a ll) = putWord8 0 >> put a >> put ll+ put (TPos pos) = putWord8 1 >> put pos+ get = do+ tag <- getWord8+ case tag of+ 0 -> liftM2 TEnemy get get+ 1 -> liftM TPos get+ _ -> fail "no parse (Target)"
+ Game/LambdaHack/Client/Strategy.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE OverloadedStrings #-}+-- | AI strategies to direct actors not controlled directly by human players.+-- No operation in this module involves the 'State' or 'Action' type.+module Game.LambdaHack.Client.Strategy+ ( Strategy, nullStrategy, liftFrequency+ , (.|), reject, (.=>), only, bestVariant, renameStrategy, returN+ ) where++import Control.Monad+import Data.Text (Text)++import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Utils.Frequency++-- | A strategy is a choice of (non-empty) frequency tables+-- of possible actions.+newtype Strategy a = Strategy { runStrategy :: [Frequency a] }+ deriving Show++-- | 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]+ m >>= f = normalizeStrategy $ Strategy+ [ toFreq name [ (p * q, b)+ | (p, a) <- runFrequency x+ , y <- runStrategy (f a)+ , (q, b) <- runFrequency y+ ]+ | x <- runStrategy m+ , let name = "Strategy_bind (" <> nameFrequency x <> ")"]++instance MonadPlus Strategy where+ mzero = Strategy []+ mplus (Strategy xs) (Strategy ys) = Strategy (xs ++ ys)++normalizeStrategy :: Strategy a -> Strategy a+normalizeStrategy (Strategy fs) = Strategy $ filter (not . nullFreq) fs++nullStrategy :: Strategy a -> Bool+nullStrategy strat = null $ runStrategy strat++-- | Strategy where only the actions from the given single frequency table+-- can be picked.+liftFrequency :: Frequency a -> Strategy a+liftFrequency f = normalizeStrategy $ Strategy $ return f++infixr 2 .|++-- | Strategy with the actions from both argument strategies,+-- with original frequencies.+(.|) :: Strategy a -> Strategy a -> Strategy a+(.|) = mplus++-- | Strategy with no actions at all.+reject :: Strategy a+reject = mzero++infix 3 .=>++-- | Conditionally accepted strategy.+(.=>) :: Bool -> Strategy a -> Strategy a+p .=> m | p = m+ | otherwise = mzero++-- | Strategy with all actions not satisfying the predicate removed.+-- The remaining actions keep their original relative frequency values.+only :: (a -> Bool) -> Strategy a -> Strategy a+only p s = normalizeStrategy $ do+ x <- s+ p x .=> return x++-- | When better choices are towards the start of the list,+-- this is the best frequency of the strategy.+bestVariant :: Strategy a -> Frequency a+bestVariant (Strategy []) = mzero+bestVariant (Strategy (f : _)) = f++-- | Overwrite the description of all frequencies within the strategy.+renameStrategy :: Text -> Strategy a -> Strategy a+renameStrategy newName (Strategy fs) = Strategy $ map (renameFreq newName) fs++-- | Like 'return', but pick a name of the single frequency.+returN :: Text -> a -> Strategy a+returN name x = Strategy $ return $ uniformFreq name [x]
+ Game/LambdaHack/Client/StrategyAction.hs view
@@ -0,0 +1,405 @@+{-# LANGUAGE OverloadedStrings #-}+-- | AI strategy operations implemented with the 'Action' monad.+module Game.LambdaHack.Client.StrategyAction+ ( targetStrategy, actionStrategy, visibleFoes+ ) where++import Control.Monad+import qualified Data.EnumMap.Strict as EM+import Data.Function+import Data.List+import Data.Maybe++import Game.LambdaHack.Client.Action+import Game.LambdaHack.Client.State+import Game.LambdaHack.Client.Strategy+import Game.LambdaHack.Common.Ability (Ability)+import qualified Game.LambdaHack.Common.Ability as Ability+import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+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.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.ServerCmd+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.ItemKind+import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.Frequency++-- TODO: express many (all?) functions as MonadActionRO++-- | AI proposes possible targets for the actor. Never empty.+targetStrategy :: MonadClient m+ => ActorId -> [Ability]+ -> m (Strategy (Maybe Target))+targetStrategy actor factionAbilities = do+ btarget <- getsClient $ getTarget actor+ fper <- getsClient sfper+ reacquireTgt fper actor btarget factionAbilities++visibleFoes :: MonadActionRO m+ => FactionPers -> ActorId -> m [(ActorId, Actor)]+visibleFoes fper aid = do+ b <- getsState $ getActorBody aid+ 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 (actorSeesLoc per aid . bpos . snd) foes++reacquireTgt :: MonadActionRO m+ => FactionPers -> ActorId -> Maybe Target -> [Ability]+ -> m (Strategy (Maybe Target))+reacquireTgt fper aid btarget factionAbilities = do+ cops@Kind.COps{coactor=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 fper aid+ 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) <- getsState $ \s ->+ (Just . TPos . (bpos b `shift`)) `liftM` moveStrategy cops aid s Nothing+ let per = fper EM.! blid b+ mk = okind $ bkind b+ actorAbilities = acanDo mk `intersect` factionAbilities+ focused = actorSpeed coactor 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 | actorSeesLoc 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 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++-- | AI strategy based on actor's sight, smell, intelligence, etc. Never empty.+actionStrategy :: MonadClient m+ => ActorId -> [Ability]+ -> m (Strategy CmdSer)+actionStrategy actor factionAbilities = do+ cops <- getsState scops+ s <- getState+ btarget <- getsClient $ getTarget actor+ disco <- getsClient sdisco+ return $! proposeAction cops actor btarget disco s factionAbilities++proposeAction :: Kind.COps -> ActorId+ -> Maybe Target -> Discovery -> State -> [Ability]+ -> Strategy CmdSer+proposeAction cops actor btarget disco s factionAbilities =+ sumS prefix .| combineDistant distant .| sumS suffix+ .| waitBlockNow actor -- wait until friends sidestep, ensures never empty+ where+ Kind.COps{coactor=Kind.Ops{okind}} = cops+ Actor{bkind, bpos} = getActorBody actor s+ (fpos, foeVisible) =+ case btarget of+ Just (TEnemy _ l) -> (l, True)+ Just (TPos l) -> (l, False)+ Nothing -> (bpos, False) -- an actor blocked by friends or a missile+ combineDistant as = liftFrequency $ sumF as+ -- TODO: Ranged and Tools should only be triggered in some situations.+ aFrequency :: Ability -> Frequency CmdSer+ aFrequency Ability.Ranged = if foeVisible+ then rangedFreq cops actor disco s fpos+ else mzero+ aFrequency Ability.Tools = if foeVisible+ then toolsFreq cops actor disco s+ else mzero+ aFrequency Ability.Chase = if fpos /= bpos+ then chaseFreq+ else mzero+ aFrequency _ = assert `failure` distant+ chaseFreq =+ scaleFreq 30 $ bestVariant $ chase cops actor s (fpos, foeVisible)+ aStrategy :: Ability -> Strategy CmdSer+ aStrategy Ability.Track = track cops actor s+ aStrategy Ability.Heal = mzero -- TODO+ aStrategy Ability.Flee = mzero -- TODO+ aStrategy Ability.Melee = foeVisible .=> melee actor s fpos+ aStrategy Ability.Pickup = not foeVisible .=> pickup actor s+ aStrategy Ability.Wander = wander cops actor s+ aStrategy _ = assert `failure` actorAbilities+ actorAbilities = acanDo (okind bkind) `intersect` factionAbilities+ isDistant = (`elem` [Ability.Ranged, Ability.Tools, Ability.Chase])+ (prefix, rest) = break isDistant actorAbilities+ (distant, suffix) = partition isDistant rest+ sumS = msum . map aStrategy+ sumF = msum . map aFrequency++-- | A strategy to always just wait.+waitBlockNow :: ActorId -> Strategy CmdSer+waitBlockNow actor = returN "wait" $ WaitSer actor++-- | Strategy for dumb missiles.+track :: Kind.COps -> ActorId -> State -> Strategy CmdSer+track cops actor s =+ strat+ where+ lvl = sdungeon s EM.! blid+ b@Actor{bpos, bpath, blid} = getActorBody actor s+ clearPath = returN "ClearPathSer" $ SetPathSer actor []+ strat = case bpath of+ Just [] -> assert `failure` (actor, b, s)+ -- TODO: instead let server do this in MoveSer, abort and handle in loop:+ Just (d : _) | not $ accessibleDir cops lvl bpos d -> clearPath+ Just lv -> returN "SetPathSer" $ SetPathSer actor lv+ Nothing -> reject++pickup :: ActorId -> State -> Strategy CmdSer+pickup actor s =+ lootHere bpos .=> actionPickup+ where+ lvl = sdungeon s EM.! blid+ body@Actor{bpos, blid} = getActorBody actor s+ lootHere x = not $ EM.null $ lvl `atI` x+ actionPickup = case EM.minViewWithKey $ lvl `atI` bpos of+ Nothing -> assert `failure` (actor, bpos, lvl)+ Just ((iid, k), _) -> -- pick up first item+ let item = getItemBody iid s+ l = if jsymbol item == '$' then Just $ InvChar '$' else Nothing+ in case assignLetter iid l body of+ Just l2 -> returN "pickup" $ PickupSer actor iid k l2+ Nothing -> returN "pickup" $ WaitSer actor++melee :: ActorId -> State -> Point -> Strategy CmdSer+melee actor s fpos =+ foeAdjacent .=> returN "melee" (MoveSer actor dir)+ where+ Level{lxsize} = sdungeon s EM.! blid+ Actor{bpos, blid} = getActorBody actor s+ foeAdjacent = adjacent lxsize bpos fpos+ dir = displacement bpos fpos++rangedFreq :: Kind.COps -> ActorId -> Discovery -> State -> Point+ -> Frequency CmdSer+rangedFreq cops actor disco s fpos =+ toFreq "throwFreq" $+ case bl of+ Just (pos1 : _) ->+ if not foesAdj+ && asight mk+ && accessible cops lvl bpos pos1 -- first accessible+ && isNothing (posToActor pos1 blid s) -- no friends on first+ then throwFreq bbag 3 (actorContainer actor binv)+ ++ throwFreq tis 6 (const $ CFloor blid bpos)+ else []+ _ -> []+ where+ Kind.COps{ coactor=Kind.Ops{okind}+ , coitem=Kind.Ops{okind=iokind}+ , corule+ } = cops+ lvl@Level{lxsize, lysize} = sdungeon s EM.! blid+ Actor{bkind, bpos, bfid, blid, bbag, binv} = getActorBody actor s+ mk = okind bkind+ tis = lvl `atI` bpos+ fact = sfactionD s EM.! bfid+ foes = actorNotProjAssocs (isAtWar fact) blid s+ foesAdj = foesAdjacent lxsize lysize bpos (map snd foes)+ -- 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.+ eps = 0+ bl = bla lxsize lysize eps bpos fpos -- TODO:make an arg of projectGroupItem+ throwFreq bag multi container =+ [ (- benefit * multi,+ ProjectSer actor 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+ Effect.effectToBenefit (jeffect i)+ Just _ki ->+ let _kik = iokind _ki+ _unneeded = isymbol _kik+ in Effect.effectToBenefit (jeffect i)+ , benefit < 0+ -- Wasting weapons and armour would be too cruel to the player.+ , jsymbol i `elem` ritemProject (Kind.stdRuleset corule) ]++toolsFreq :: Kind.COps -> ActorId -> Discovery -> State -> Frequency CmdSer+toolsFreq cops actor disco s =+ toFreq "quaffFreq"+ $ quaffFreq bbag 1 (actorContainer actor binv)+ ++ quaffFreq tis 2 (const $ CFloor blid bpos)+ where+ Kind.COps{coitem=Kind.Ops{okind=_iokind}} = cops+ Actor{bpos, blid, bbag, binv} = getActorBody actor s+ lvl = sdungeon s EM.! blid+ tis = lvl `atI` bpos+ quaffFreq bag multi container =+ [ (benefit * multi, ApplySer actor iid (container iid))+ | (iid, i) <- map (\iid -> (iid, getItemBody iid s))+ $ EM.keys bag+ , let benefit =+ case jkind disco i of+ Nothing -> 30 -- experimenting is fun+ Just _ki -> Effect.effectToBenefit $ jeffect i+ , benefit > 0+ , jsymbol i == '!' ]++-- 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 :: Kind.COps -> ActorId -> State -> Maybe (Point, Bool)+ -> Strategy Vector+moveStrategy cops actor s mFoe =+ 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 actor 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+ 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 5 $ bestVariant $ onlyInterest $ onlyKeepsDir 2 moveRandomly+ interestIQFreq = interestFreq `mplus` bestVariant moveIQ+ moveClear = onlyMoves (not . bumpableHere cops lvl foeVisible) moveFreely+ moveOpenable = onlyMoves (bumpableHere cops lvl foeVisible) 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 l)+ sensible = filter (isSensible . (bpos `shift`)) (moves lxsize)++bumpableHere :: Kind.COps -> Level -> Bool -> Point -> Bool+bumpableHere Kind.COps{cotile} lvl foeVisible pos =+ let t = lvl `at` pos+ in Tile.hasFeature cotile F.Openable t+ || -- Try to find hidden doors only if no foe in sight.+ not foeVisible && Tile.hasFeature cotile F.Suspect t++chase :: Kind.COps -> ActorId -> State -> (Point, Bool) -> Strategy CmdSer+chase cops actor s foe@(_, foeVisible) =+ -- 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.+ -- TODO: explore if a possible secret+ let mFoe = Just foe+ fight = not foeVisible -- don't pick fights if the real foe is close+ in if fight+ then ExploreSer actor `liftM` moveStrategy cops actor s mFoe+ else RunSer actor `liftM` moveStrategy cops actor s mFoe++wander :: Kind.COps -> ActorId -> State -> Strategy CmdSer+wander cops actor s =+ -- Target set, but we don't chase the foe, e.g., because we are blocked+ -- or we cannot chase at all.+ -- TODO: explore if a possible secret+ let mFoe = Nothing+ in ExploreSer actor `liftM` moveStrategy cops actor s mFoe
− Game/LambdaHack/Color.hs
@@ -1,126 +0,0 @@--- | Colours and text attributes.-module Game.LambdaHack.Color- ( -- * Colours- Color(..), defBG, defFG, isBright, legalBG, colorToRGB- -- * Text attributes and the screen- , Attr(..), defaultAttr, AttrChar(..)- ) where--import Data.Binary---- TODO: since this type may be essential to speed, consider implementing--- it as an Int, with color numbered as they are on terminals, see--- http://www.haskell.org/haskellwiki/Performance/Data_types#Enumerations--- If we ever switch to 256 colours, the Int implementation or similar--- will be more natural, anyway.--- | Colours supported by the major frontends.-data Color =- Black- | Red- | Green- | Brown- | Blue- | Magenta- | Cyan- | White- | BrBlack- | BrRed- | BrGreen- | BrYellow- | BrBlue- | BrMagenta- | BrCyan- | BrWhite- deriving (Show, Eq, Ord, Enum, Bounded)--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-defFG = White---- | Text attributes: foreground and backgroud colors.-data Attr = Attr- { fg :: !Color -- ^ foreground colour- , bg :: !Color -- ^ backgroud color- }- deriving (Show, Eq, Ord)--instance Binary Attr where- put Attr{..} = do- put fg- put bg- get = do- fg <- get- bg <- get- return Attr{..}---- | The default attribute, to optimize attribute setting.-defaultAttr :: Attr-defaultAttr = Attr defFG defBG--data AttrChar = AttrChar- { acAttr :: !Attr- , acChar :: !Char- }- deriving (Show, Eq)--instance Binary AttrChar where- put AttrChar{..} = do- put acAttr- put acChar- get = do- acAttr <- get- acChar <- get- return AttrChar{..}---- | A helper for the terminal frontends that display bright via bold.-isBright :: Color -> Bool-isBright c = c >= BrBlack---- | Due to the limitation of the curses library used in the curses frontend,--- only these are legal backgrounds.-legalBG :: [Color]-legalBG = [Black, White, Blue, Magenta]---- | Translationg to heavily modified Linux console color RGB values.-colorToRGB :: Color -> String-colorToRGB Black = "#000000"-colorToRGB Red = "#D50000"-colorToRGB Green = "#00AA00"-colorToRGB Brown = "#AA5500"-colorToRGB Blue = "#203AF0"-colorToRGB Magenta = "#AA00AA"-colorToRGB Cyan = "#00AAAA"-colorToRGB White = "#C5BCB8"-colorToRGB BrBlack = "#6F5F5F"-colorToRGB BrRed = "#FF5555"-colorToRGB BrGreen = "#75FF45"-colorToRGB BrYellow = "#FFE855"-colorToRGB BrBlue = "#4090FF"-colorToRGB BrMagenta = "#FF77FF"-colorToRGB BrCyan = "#60FFF0"-colorToRGB BrWhite = "#FFFFFF"---- | For reference, the original Linux console colors.--- Good old retro feel and more useful than xterm (e.g. brown).-_olorToRGB :: Color -> String-_olorToRGB Black = "#000000"-_olorToRGB Red = "#AA0000"-_olorToRGB Green = "#00AA00"-_olorToRGB Brown = "#AA5500"-_olorToRGB Blue = "#0000AA"-_olorToRGB Magenta = "#AA00AA"-_olorToRGB Cyan = "#00AAAA"-_olorToRGB White = "#AAAAAA"-_olorToRGB BrBlack = "#555555"-_olorToRGB BrRed = "#FF5555"-_olorToRGB BrGreen = "#55FF55"-_olorToRGB BrYellow = "#FFFF55"-_olorToRGB BrBlue = "#5555FF"-_olorToRGB BrMagenta = "#FF55FF"-_olorToRGB BrCyan = "#55FFFF"-_olorToRGB BrWhite = "#FFFFFF"
− Game/LambdaHack/Command.hs
@@ -1,108 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Abstract syntax of player commands.-module Game.LambdaHack.Command- ( Cmd(..), majorCmd, timedCmd, cmdDescription- ) where--import Data.Text (Text)-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.Assert-import qualified Game.LambdaHack.Feature as F-import Game.LambdaHack.Msg---- | Abstract syntax of player commands. The type is abstract, but the values--- are created outside this module via the Read class (from config file) .-data Cmd =- -- These take time.- Apply { verb :: MU.Part, object :: MU.Part, syms :: [Char] }- | Project { verb :: MU.Part, object :: MU.Part, syms :: [Char] }- | TriggerDir { verb :: MU.Part, object :: MU.Part, feature :: F.Feature }- | TriggerTile { verb :: MU.Part, object :: MU.Part, feature :: F.Feature }- | Pickup- | Drop- | Wait- | GameExit- | GameRestart- -- These do not take time, or not quite.- | GameSave- | Inventory- | TgtFloor- | TgtEnemy- | TgtAscend Int- | EpsIncr Bool- | Cancel- | Accept- | Clear- | History- | CfgDump- | HeroCycle- | HeroBack- | Help- deriving (Show, Read, Eq, Ord)---- | Major commands land on the first page of command help.-majorCmd :: Cmd -> Bool-majorCmd cmd = case cmd of- Apply{} -> True- Project{} -> True- TriggerDir{} -> True- TriggerTile{} -> True- Pickup -> True- Drop -> True- GameExit -> True- GameRestart -> True- GameSave -> True- Inventory -> True- Help -> True- _ -> False---- | Time cosuming commands are marked as such in help and cannot be--- invoked in targeting mode on a remote level (level different than--- the level of the selected hero).-timedCmd :: Cmd -> Bool-timedCmd cmd = case cmd of- Apply{} -> True- Project{} -> True- TriggerDir{} -> True- TriggerTile{} -> True- Pickup -> True- Drop -> True- Wait -> True- GameExit -> True -- takes time, then rewinds time- GameRestart -> True -- takes time, then resets state- _ -> False---- | Description of player commands.-cmdDescription :: Cmd -> Text-cmdDescription cmd = case cmd of- Apply{..} -> makePhrase [verb, MU.AW object]- Project{..} -> makePhrase [verb, MU.AW object]- TriggerDir{..} -> makePhrase [verb, MU.AW object]- TriggerTile{..} -> makePhrase [verb, MU.AW object]- Pickup -> "get an object"- Drop -> "drop an object"- Wait -> ""- GameExit -> "save and exit"- GameRestart -> "restart game"- GameSave -> "save game"-- Inventory -> "display inventory"- TgtFloor -> "target location"- 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 in targeting in config file" :: Text)- EpsIncr True -> "swerve targeting line"- EpsIncr False -> "unswerve targeting line"- Cancel -> "cancel action"- Accept -> "accept choice"- Clear -> "clear messages"- History -> "display previous messages"- CfgDump -> "dump current configuration"- HeroCycle -> "cycle among heroes on level"- HeroBack -> "cycle among heroes in the dungeon"- Help -> "display help"
− Game/LambdaHack/CommandAction.hs
@@ -1,73 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Semantics of player commands.-module Game.LambdaHack.CommandAction- ( configCmds, semanticsCmds- ) where--import Control.Monad.State hiding (State, state)-import qualified Data.List as L-import Data.Text (Text)--import Game.LambdaHack.Action-import Game.LambdaHack.Actions-import Game.LambdaHack.ItemAction-import Game.LambdaHack.State-import Game.LambdaHack.Command-import Game.LambdaHack.Config-import qualified Game.LambdaHack.Key as K---- | The semantics of player commands in terms of the @Action@ monad.-cmdAction :: Cmd -> ActionFrame ()-cmdAction cmd = case cmd of- Apply{..} -> inFrame $ playerApplyGroupItem verb object syms- Project{..} -> playerProjectGroupItem verb object syms- TriggerDir{..} -> inFrame $ playerTriggerDir feature verb- TriggerTile{..} -> inFrame $ playerTriggerTile feature- Pickup -> inFrame $ pickupItem- Drop -> inFrame $ dropItem- Wait -> inFrame $ waitBlock- GameExit -> inFrame $ gameExit- GameRestart -> inFrame $ gameRestart- GameSave -> inFrame $ gameSave-- Inventory -> inventory- TgtFloor -> targetFloor TgtExplicit- TgtEnemy -> targetMonster TgtExplicit- TgtAscend k -> tgtAscend k- EpsIncr b -> inFrame $ epsIncr b- Cancel -> cancelCurrent displayMainMenu- Accept -> acceptCurrent displayHelp- Clear -> inFrame $ clearCurrent- History -> displayHistory- CfgDump -> inFrame $ dumpConfig- HeroCycle -> inFrame $ cycleHero- HeroBack -> inFrame $ backCycleHero- Help -> displayHelp---- | The associaction of commands to keys defined in config.-configCmds :: ConfigUI -> [(K.Key, Cmd)]-configCmds ConfigUI{configCommands} =- let mkCommand (key, def) = (key, read def :: Cmd)- in L.map mkCommand configCommands---- | The list of semantics and other info for all commands from config.-semanticsCmds :: [(K.Key, Cmd)]- -> [((K.Key, K.Modifier), (Text, Bool, ActionFrame ()))]-semanticsCmds cmdList =- let mkDescribed cmd =- let semantics = if timedCmd cmd- then checkCursor $ cmdAction cmd- else cmdAction cmd- in (cmdDescription cmd, timedCmd cmd, semantics)- mkCommand (key, def) = ((key, K.NoModifier), mkDescribed def)- in L.map mkCommand cmdList---- | If in targeting mode, check if the current level is the same--- as player level and refuse performing the action otherwise.-checkCursor :: ActionFrame () -> ActionFrame ()-checkCursor h = do- cursor <- gets scursor- slid <- gets slid- if creturnLn cursor == slid- then h- else abortWith "[targeting] you inspect a remote level, press ESC to switch back"
+ Game/LambdaHack/Common/Ability.hs view
@@ -0,0 +1,25 @@+-- | AI strategy abilities.+module Game.LambdaHack.Common.Ability+ ( Ability(..)+ ) where++-- | All possible AI actor abilities. AI chooses among these when considering+-- the next action to perform. The ability descriptions refer to the target+-- 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+ | Heal -- ^ heal if almost dead+ | Flee -- ^ flee if almost dead+ | Melee -- ^ melee target+ | Pickup -- ^ gather items, if no foes visible+ | Ranged -- ^ attack the visible target opponent at range, some of the time+ | Tools -- ^ use items, if target opponent visible, some of the time+ | Chase -- ^ chase the target, ignoring any actors on the way+ | Wander -- ^ wander around, meleeing any opponents on the way+ deriving (Show, Eq, Ord, Enum, Bounded)++ -- A note for when we let AI change levels:+ -- A faction that spawns cannot switch levels (nor move between levels).+ -- Otherwise it would constantly go to a distant level, spawn actors there+ -- and swarm any opponent arriving on the level.
+ Game/LambdaHack/Common/Action.hs view
@@ -0,0 +1,49 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Game action monads and basic building blocks for human and computer+-- player actions. Has no access to the the main action type.+module Game.LambdaHack.Common.Action+ ( -- * Action monads+ MonadActionRO(..), MonadAction(..), MonadAtomic(..)+ -- * Shorthands+ , getsLevel, nHumans+ ) 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+import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.State++class (Monad m, Functor m) => MonadActionRO m where+ 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 ()++class MonadActionRO m => MonadAtomic m where+ execAtomic :: Atomic -> m ()+ execCmdAtomic :: CmdAtomic -> m ()+ execCmdAtomic = execAtomic . CmdAtomic+ execSfxAtomic :: SfxAtomic -> m ()+ execSfxAtomic = execAtomic . SfxAtomic++getsLevel :: MonadActionRO m => LevelId -> (Level -> a) -> m a+getsLevel lid f = getsState $ f . (EM.! lid) . sdungeon++nHumans :: MonadActionRO m => m Int+nHumans = do+ factionD <- getsState sfactionD+ return $ length $ filter isHumanFact $ EM.elems factionD
+ Game/LambdaHack/Common/Actor.hs view
@@ -0,0 +1,274 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+-- | Actors in the game: heroes, monsters, etc. No operation in this module+-- involves the 'State' or 'Action' type.+module Game.LambdaHack.Common.Actor+ ( -- * Actor identifiers and related operations+ ActorId, monsterGenChance, partActor+ -- * The@ Acto@r type+ , Actor(..), actorTemplate, timeAddFromSpeed, braced+ , unoccupied, heroKindId, projectileKindId, actorSpeed+ -- * Inventory management+ , ItemBag, ItemInv, InvChar(..), ItemDict, ItemRev+ , allLetters, assignLetter, letterLabel, letterRange, rmFromBag+ -- * Assorted+ , ActorDict, smellTimeout, mapActorItems_+ ) where++import Data.Binary+import Data.Char+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import qualified Data.Hashable as Hashable+import qualified Data.HashMap.Strict as HM+import Data.List+import Data.Maybe+import Data.Ratio+import Data.Text (Text)+import qualified Data.Text as T+import Data.Tuple+import qualified NLP.Miniutter.English as MU++import qualified Game.LambdaHack.Common.Color as Color+import Game.LambdaHack.Common.Item+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Utils.Assert++-- | A unique identifier of an actor in the dungeon.+newtype ActorId = ActorId Int+ deriving (Show, Eq, Ord, Enum)++instance Binary ActorId where+ put (ActorId n) = put n+ get = fmap ActorId get++-- | Actor properties that are changing throughout the game.+-- If they are dublets of properties from @ActorKind@,+-- 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 :: !(Maybe Char) -- ^ individual map symbol+ , _bname :: !(Maybe Text) -- ^ individual name+ , bcolor :: !(Maybe Color.Color) -- ^ individual map color+ , bspeed :: !(Maybe 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)+ }+ deriving (Show, Eq, Ord)++-- | Chance that a new monster is generated. Currently depends on the+-- number of monsters already present, and on the level. In the future,+-- the strength of the character and the strength of the monsters present+-- could further influence the chance, and the chance could also affect+-- which monster is generated. How many and which monsters are generated+-- will also depend on the cave kind used to build the level.+monsterGenChance :: Int -> Int -> Int -> Rnd Bool+monsterGenChance ldepth depth numMonsters =+ -- Mimics @rollDeep@.+ let scaledDepth = 10 * (ldepth - 1) `div` max 1 (depth - 1)+ in chance $ 1%(fromIntegral (50 * (numMonsters - scaledDepth)) `max` 5)++-- | The part of speech describing the actor.+partActor :: Kind.Ops ActorKind -> Actor -> MU.Part+partActor Kind.Ops{oname} a =+ case _bname a of+ Nothing -> MU.AW $ MU.Text $ oname $ bkind a+ Just properName -> MU.Text properName++-- Actor operations++-- | A template for a new non-projectile actor.+actorTemplate :: Kind.Id ActorKind -> Maybe Char -> Maybe Text+ -> Maybe Color.Color -> Maybe Speed -> Int -> Maybe [Vector]+ -> Point -> LevelId -> Time -> FactionId -> Bool -> Actor+actorTemplate bkind bsymbol _bname bcolor bspeed bhp bpath bpos blid btime+ bfid bproj =+ let boldpos = bpos+ bbag = EM.empty+ binv = EM.empty+ bletter = InvChar 'a'+ bwait = timeZero+ in Actor{..}++-- | Access actor speed, individual or, otherwise, stock.+actorSpeed :: Kind.Ops ActorKind -> Actor -> Speed+actorSpeed Kind.Ops{okind} m =+ let stockSpeed = aspeed $ okind $ bkind m+ in fromMaybe stockSpeed $ bspeed m++-- | Add time taken by a single step at the actor's current speed.+timeAddFromSpeed :: Kind.Ops ActorKind -> Actor -> Time -> Time+timeAddFromSpeed coactor m time =+ let speed = actorSpeed coactor m+ delta = ticksPerMeter speed+ in timeAdd time delta++-- | Whether an actor is braced for combat this turn.+braced :: Actor -> Time -> Bool+braced m time = time < bwait m++-- | Checks for the presence of actors in a position.+-- Does not check if the tile is walkable.+unoccupied :: [Actor] -> Point -> Bool+unoccupied actors pos = all (\body -> bpos body /= pos) actors++-- | The unique kind of heroes.+heroKindId :: Kind.Ops ActorKind -> Kind.Id ActorKind+heroKindId Kind.Ops{ouniqGroup} = ouniqGroup "hero"++-- | The unique kind of projectiles.+projectileKindId :: Kind.Ops ActorKind -> Kind.Id ActorKind+projectileKindId Kind.Ops{ouniqGroup} = ouniqGroup "projectile"++-- | How long until an actor's smell vanishes from a tile.+smellTimeout :: Time+smellTimeout = timeScale timeTurn 100++newtype InvChar = InvChar {invChar :: Char}+ deriving (Show, Eq, Enum)++instance Ord InvChar where+ compare (InvChar x) (InvChar y) =+ compare (isUpper x, toLower x) (isUpper y, toLower y)++instance Binary InvChar where+ put (InvChar x) = put x+ get = fmap InvChar get++type ItemBag = EM.EnumMap ItemId Int++type ItemInv = EM.EnumMap InvChar ItemId++-- | All items in the dungeon (including in actor inventories),+-- indexed by item identifier.+type ItemDict = EM.EnumMap ItemId Item++-- | All actors on the level, indexed by actor identifier.+type ActorDict = EM.EnumMap ActorId Actor++-- | Reverse item map, for item creation, to keep items and item identifiers+-- in bijection.+type ItemRev = HM.HashMap Item ItemId++instance (Binary k, Binary v, Eq k, Hashable.Hashable k)+ => Binary (HM.HashMap k v) where+ put ir = put $ HM.toList ir+ get = fmap HM.fromList get++cmpLetter :: InvChar -> InvChar -> Ordering+cmpLetter (InvChar x) (InvChar y) =+ compare (isUpper x, toLower x) (isUpper y, toLower y)++allLetters :: [InvChar]+allLetters = map InvChar $ ['a'..'z'] ++ ['A'..'Z']++-- | Assigns a letter to an item, for inclusion in the inventory+-- of a hero. Tries to to use the requested letter, if any.+assignLetter :: ItemId -> Maybe InvChar -> Actor -> Maybe InvChar+assignLetter iid r body =+ case lookup iid $ map swap $ EM.assocs $ binv body of+ Just l -> Just l+ Nothing -> case r of+ Just l | l `elem` allowed -> Just l+ _ -> listToMaybe free+ where+ c = bletter body+ candidates = take (length allLetters)+ $ drop (fromJust (elemIndex c allLetters))+ $ cycle allLetters+ inBag = EM.keysSet $ bbag body+ f l = maybe True (`ES.notMember` inBag) $ EM.lookup l $ binv body+ free = filter f candidates+ allowed = InvChar '$' : free++letterRange :: [InvChar] -> Text+letterRange ls =+ sectionBy (sortBy cmpLetter ls) Nothing+ where+ succLetter c d = ord (invChar d) - ord (invChar c) == 1++ sectionBy [] Nothing = T.empty+ sectionBy [] (Just (c, d)) = finish (c,d)+ sectionBy (x:xs) Nothing = sectionBy xs (Just (x, x))+ sectionBy (x:xs) (Just (c, d))+ | succLetter d x = sectionBy xs (Just (c, x))+ | otherwise = finish (c,d) <> sectionBy xs (Just (x, x))++ finish (c, d) | c == d = T.pack [invChar c]+ | succLetter c d = T.pack [invChar c, invChar d]+ | otherwise = T.pack [invChar c, '-', invChar d]++letterLabel :: InvChar -> MU.Part+letterLabel c = MU.Text $ T.pack $ invChar c : " -"++rmFromBag :: Int -> ItemId -> ItemBag -> ItemBag+rmFromBag k iid bag =+ let rib Nothing = assert `failure` (k, iid, bag)+ rib (Just n) = case compare n k of+ LT -> assert `failure` (n, k, iid, bag)+ EQ -> Nothing+ GT -> Just (n - k)+ in EM.alter rib iid bag++mapActorItems_ :: Monad m => (ItemId -> Int -> m a) -> Actor -> m ()+mapActorItems_ f Actor{bbag} = do+ let is = EM.assocs bbag+ mapM_ (uncurry f) is++instance Binary Actor where+ put Actor{..} = do+ put bkind+ put bsymbol+ put _bname+ put bcolor+ put bspeed+ put bhp+ put bpath+ put bpos+ put boldpos+ put blid+ put bbag+ put binv+ put bletter+ put btime+ put bwait+ put bfid+ put bproj+ get = do+ bkind <- get+ bsymbol <- get+ _bname <- get+ bcolor <- get+ bspeed <- get+ bhp <- get+ bpath <- get+ bpos <- get+ boldpos <- get+ blid <- get+ bbag <- get+ binv <- get+ bletter <- get+ btime <- get+ bwait <- get+ bfid <- get+ bproj <- get+ return Actor{..}
+ Game/LambdaHack/Common/ActorState.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Operations on the 'Actor' type that need the 'State' type,+-- but not the 'Action' type.+-- TODO: Document an export list after it's rewritten according to #17.+module Game.LambdaHack.Common.ActorState+ ( actorAssocs, actorList, actorNotProjAssocs, actorNotProjList+ , calculateTotal, nearbyFreePoints, whereTo+ , posToActor, getItemBody, memActor, getActorBody, updateActorBody+ , getActorItem, getActorBag, actorContainer, getActorInv+ , tryFindHeroK, foesAdjacent+ ) where++import qualified Data.Char as Char+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.List+import Data.Maybe++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.Content.TileKind+import Game.LambdaHack.Utils.Assert++actorAssocs :: (FactionId -> Bool) -> LevelId -> State+ -> [(ActorId, Actor)]+actorAssocs p lid s =+ mapMaybe (\aid -> let actor = sactorD s EM.! aid+ in if p (bfid actor)+ then Just (aid, actor)+ else Nothing)+ $ concat $ EM.elems $ lprio $ sdungeon s EM.! lid++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+ in if not (bproj actor) && p (bfid actor)+ then Just (aid, actor)+ else Nothing)+ $ concat $ EM.elems $ lprio $ sdungeon s EM.! lid++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` l) $+ listToMaybe l++posToActors :: Point -> LevelId -> State -> [ActorId]+posToActors pos lid s =+ let as = actorAssocs (const True) lid s+ apos = filter (\(_, b) -> bpos b == pos) as+ in fmap fst apos++nearbyFreePoints :: Kind.Ops TileKind+ -> (Kind.Id TileKind -> Bool) -> Point -> LevelId -> State+ -> [Point]+nearbyFreePoints cotile f start lid s =+ let lvl@Level{lxsize, lysize} = sdungeon s EM.! lid+ good p = f (lvl `at` p)+ && Tile.hasFeature cotile F.Walkable (lvl `at` p)+ && unoccupied (actorList (const True) lid s) p+ ps = nub $ start : concatMap (vicinity lxsize lysize) ps+ in filter good ps++-- | Calculate loot's worth for heroes on the current level.+--+-- Warning: scores are shown during the game, so when the server calculates+-- then, we should be careful not to leak secret information+-- (e.g., the nature of the items through the total worth of inventory).+calculateTotal :: Actor -> State -> (ItemBag, Int)+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)++-- | Price an item, taking count into consideration.+itemPrice :: (Item, Int) -> Int+itemPrice (item, jcount) =+ case jsymbol item of+ '$' -> jcount+ '*' -> jcount * 100+ _ -> 0++foesAdjacent :: X -> Y -> Point -> [Actor] -> Bool+foesAdjacent lxsize lysize pos foes =+ let vic = ES.fromList $ vicinity lxsize lysize pos+ lfs = ES.fromList $ map bpos foes+ in not $ ES.null $ ES.intersection vic lfs++-- * These few operations look at, potentially, all levels of the dungeon.++-- | Tries to finds an actor body satisfying a predicate on any level.+tryFindActor :: State -> (Actor -> Bool) -> Maybe (ActorId, Actor)+tryFindActor s p =+ find (p . snd) $ EM.assocs $ sactorD s++tryFindHeroK :: State -> FactionId -> Int -> Maybe (ActorId, Actor)+tryFindHeroK s fact k =+ let c | k == 0 = '@'+ | k > 0 && k < 10 = Char.intToDigit k+ | otherwise = assert `failure` k+ in tryFindActor s (\body -> bsymbol body == Just c+ && not (bproj body)+ && bfid body == fact)++-- | Compute the level identifier and starting position on the level,+-- after a level change.+whereTo :: State -- ^ game state+ -> LevelId -- ^ start from this level+ -> Int -- ^ jump down this many levels+ -> Maybe (LevelId, Point)+ -- ^ target level and the position of its receiving stairs+whereTo s lid k = assert (k /= 0) $+ case ascendInBranch (sdungeon s) lid k of+ [] -> Nothing+ ln : _ -> let lvlTrg = sdungeon s EM.! ln+ in Just (ln, (if k < 0 then fst else snd) (lstair lvlTrg))++-- * The operations below disregard levels other than the current.++-- | Gets actor body from the current level. Error if not found.+getActorBody :: ActorId -> State -> Actor+getActorBody aid s =+ fromMaybe (assert `failure` (aid, s)) $ EM.lookup aid $ sactorD s++updateActorBody :: ActorId -> (Actor -> Actor) -> State -> State+updateActorBody aid f s =+ let alt Nothing = assert `failure` (aid, s)+ alt (Just b) = Just $ f b+ in updateActorD (EM.alter alt aid) s++getActorBag :: ActorId -> State -> ItemBag+getActorBag aid s = bbag $ getActorBody aid s++actorContainer :: ActorId -> ItemInv -> ItemId -> Container+actorContainer aid binv iid =+ case find ((== iid) . snd) $ EM.assocs binv of+ Just (l, _) -> CActor aid l+ Nothing -> assert `failure` (aid, binv, iid)++getActorInv :: ActorId -> State -> ItemInv+getActorInv aid s = binv $ getActorBody aid s++-- | Gets actor's items from the current level. Warning: this does not work+-- for viewing items of actors from remote level.+getActorItem :: ActorId -> State -> [(ItemId, Item)]+getActorItem aid s =+ let f iid = (iid, getItemBody iid s)+ in map f $ EM.keys $ getActorBag aid s++getItemBody :: ItemId -> State -> Item+getItemBody iid s =+ fromMaybe (assert `failure` (iid, s)) $ EM.lookup iid $ sitemD s++-- | Checks if the actor is present on the current level.+-- The order of argument here and in other functions is set to allow+--+-- > b <- getsState (memActor a)+memActor :: ActorId -> LevelId -> State -> Bool+memActor aid lid s =+ maybe False ((== lid) . blid) $ EM.lookup aid $ sactorD s
+ Game/LambdaHack/Common/Animation.hs view
@@ -0,0 +1,219 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}+-- | Screen frames and animations.+module Game.LambdaHack.Common.Animation+ ( Attr(..), defAttr, AttrChar(..)+ , SingleFrame(..), emptySingleFrame, xsizeSingleFrame, ysizeSingleFrame+ , Animation, Frames, renderAnim, restrictAnim+ , twirlSplash, blockHit, blockMiss, deathBody, swapPlaces, fadeout+ , AcFrame(..)+ ) where++import Control.Arrow ((***))+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.Maybe+import Data.Monoid+import Data.Text (Text)++import Game.LambdaHack.Common.Color+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random++-- | The data sufficent to draw a single game screen frame.+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+ }+ 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{..}++-- | Animation is a list of frame modifications to play one by one,+-- where each modification if a map from positions to level map symbols.+newtype Animation = Animation [EM.EnumMap Point AttrChar]+ deriving (Eq, Show, Monoid)++-- | 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++ysizeSingleFrame :: SingleFrame -> X+ysizeSingleFrame SingleFrame{sfLevel} = length sfLevel++-- | 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 fLine y lineOld =+ let f l (x, acOld) =+ let pos = toPoint lxsize (PointXY (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.+ 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 (modifyFrame basicFrame) anim++blank :: Maybe AttrChar+blank = Nothing++coloredSymbol :: Color -> Char -> Maybe AttrChar+coloredSymbol color symbol = Just $ AttrChar (Attr color defBG) symbol++mzipPairs :: (Point, Point) -> (Maybe AttrChar, Maybe AttrChar)+ -> [(Point, AttrChar)]+mzipPairs (p1, p2) (mattr1, mattr2) =+ let mzip (pos, mattr) = fmap (\x -> (pos, x)) mattr+ in catMaybes $ if p1 /= p2+ then [mzip (p1, mattr1), mzip (p2, mattr2)]+ else -- If actor affects himself, show only the effect,+ -- not the action.+ [mzip (p1, mattr1)]++restrictAnim :: ES.EnumSet Point -> Animation -> Animation+restrictAnim vis (Animation as) =+ let f imap =+ let common = EM.intersection imap $ EM.fromSet (const ()) vis+ in if EM.null common then Nothing else Just common+ in Animation $ mapMaybe f as++-- | Attack animation. A part of it also reused for self-damage and healing.+twirlSplash :: (Point, Point) -> Color -> Color -> Animation+twirlSplash poss c1 c2 = Animation $ map (EM.fromList . mzipPairs poss)+ [ (coloredSymbol BrWhite '*', blank)+ , (coloredSymbol c1 '/', coloredSymbol BrCyan '^')+ , (coloredSymbol c1 '-', blank)+ , (coloredSymbol c1 '\\',blank)+ , (coloredSymbol c1 '|', blank)+ , (coloredSymbol c2 '/', blank)+ , (coloredSymbol c2 '%', coloredSymbol BrCyan '^')+ , (coloredSymbol c2 '%', blank)+ , (blank , blank)+ ]++-- | Attack that hits through a block.+blockHit :: (Point, Point) -> Color -> Color -> Animation+blockHit poss c1 c2 = Animation $ map (EM.fromList . mzipPairs poss)+ [ (coloredSymbol BrWhite '*', blank)+ , (coloredSymbol BrBlue '{', coloredSymbol BrCyan '^')+ , (coloredSymbol BrBlue '{', blank)+ , (coloredSymbol c1 '}', blank)+ , (coloredSymbol c1 '}', coloredSymbol BrCyan '^')+ , (coloredSymbol c2 '/', blank)+ , (coloredSymbol c2 '%', blank)+ , (coloredSymbol c2 '%', blank)+ , (blank , blank)+ ]++-- | Attack that is blocked.+blockMiss :: (Point, Point) -> Animation+blockMiss poss = Animation $ map (EM.fromList . mzipPairs poss)+ [ (coloredSymbol BrWhite '*', blank)+ , (coloredSymbol BrBlue '{', coloredSymbol BrCyan '^')+ , (coloredSymbol BrBlue '}', blank)+ , (coloredSymbol BrBlue '}', blank)+ , (blank , blank)+ ]++-- | Death animation for an organic body.+deathBody :: Point -> Animation+deathBody pos = Animation $ map (maybe EM.empty (EM.singleton pos))+ [ coloredSymbol BrRed '\\'+ , coloredSymbol BrRed '\\'+ , coloredSymbol BrRed '|'+ , coloredSymbol BrRed '|'+ , coloredSymbol BrRed '%'+ , coloredSymbol BrRed '%'+ , coloredSymbol Red '%'+ , coloredSymbol Red '%'+ , coloredSymbol Red ';'+ , coloredSymbol Red ';'+ , coloredSymbol Red ','+ ]++-- | Swap-places animation, both hostile and friendly.+swapPlaces :: (Point, Point) -> Animation+swapPlaces poss = Animation $ map (EM.fromList . mzipPairs poss)+ [ (coloredSymbol BrMagenta '.', coloredSymbol Magenta 'o')+ , (coloredSymbol BrMagenta 'd', coloredSymbol Magenta 'p')+ , (coloredSymbol Magenta 'p', coloredSymbol BrMagenta 'd')+ , (coloredSymbol Magenta 'o', blank)+ ]++fadeout :: Bool -> Bool -> X -> Y -> Rnd Animation+fadeout out topRight lxsize lysize = do+ let xbound = lxsize - 1+ ybound = lysize - 1+ edge = EM.fromDistinctAscList $ zip [1..] ".%&%;:,."+ fadeChar r n x y =+ let d = x - 2 * y+ ndy = n - d - 2 * ybound+ ndx = n + d - xbound - 1 -- @-1@ for asymmetry+ mnx = if ndy > 0 && ndx > 0+ then min ndy ndx+ else max ndy ndx+ v3 = (r `xor` (x * y)) `mod` 3+ k | mnx < 3 || mnx > 10 = mnx+ | (min x (xbound - x - y) + n + v3) `mod` 15 < 11+ && mnx > 6 = mnx - v3+ | (x + 3 * y + v3) `mod` 30 < 19 = mnx + 1+ | otherwise = mnx+ 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 )+ | 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+ 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++data AcFrame =+ AcConfirm SingleFrame+ | AcRunning SingleFrame+ | AcNormal SingleFrame+ | 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)"
+ Game/LambdaHack/Common/Area.hs view
@@ -0,0 +1,62 @@+-- | Rectangular areas of levels and their basic operations.+module Game.LambdaHack.Common.Area+ ( Area, vicinityXY, vicinityCardinalXY, insideXY+ , normalizeArea, grid, validArea, trivialArea, expand+ ) where++import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.VectorXY++-- | The type of areas. The bottom left and the top right points.+type Area = (X, Y, X, Y)++-- | All (8 at most) closest neighbours of a point within an area.+vicinityXY :: Area -- ^ 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 :: Area -- ^ 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 ]++-- | Checks that a point belongs to an area.+insideXY :: PointXY -> Area -> Bool+insideXY (PointXY (x, y)) (x0, y0, x1, y1) =+ x1 >= x && x >= x0 && y1 >= y && y >= y0++-- | Sort the corners of an area so that the bottom left is the first point.+normalizeArea :: Area -> Area+normalizeArea (x0, y0, x1, y1) = (min x0 x1, min y0 y1, max x0 x1, max y0 y1)++-- | Divide uniformly a larger area into the given number of smaller areas.+grid :: (X, Y) -> Area -> [(PointXY, Area)]+grid (nx, ny) (x0, y0, x1, y1) =+ let xd = x1 - x0+ yd = y1 - y0+ -- 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.+ xborder = if nx == 1 then 3 else 2+ yborder = if ny == 1 then 3 else 2+ in [ (PointXY (x, y), (x0 + (xd * x `div` nx) + xborder,+ y0 + (yd * y `div` ny) + yborder,+ x0 + (xd * (x + 1) `div` nx) - xborder,+ y0 + (yd * (y + 1) `div` ny) - yborder))+ | x <- [0..nx-1], y <- [0..ny-1] ]++-- | Checks if it's an area with at least one field.+validArea :: Area -> Bool+validArea (x0, y0, x1, y1) = x0 <= x1 && y0 <= y1++-- | Checks if it's an area with exactly one field.+trivialArea :: Area -> Bool+trivialArea (x0, y0, x1, y1) = x0 == x1 && y0 == y1++-- | Enlarge (or shrink) the given area on all fours sides by the amount.+expand :: Area -> Int -> Area+expand (x0, y0, x1, y1) k = (x0 - k, y0 - k, x1 + k, y1 + k)
+ Game/LambdaHack/Common/AtomicCmd.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE DeriveGeneric #-}+-- | A set of atomic commands shared by client and server.+-- These are the largest building blocks that have no components+-- that can be observed in isolation.+--+-- We try to make atomic commands respect the laws of energy and mass+-- conservation, unless they really can't, e.g., monster spawning.+-- For example item removal from inventory is not an atomic command,+-- but item dropped from the inventory to the ground is. This makes+-- it easier to undo the commands. In principle, the commands are the only+-- way to affect the basic game state (@State@).+--+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Common.AtomicCmd+ ( Atomic(..), CmdAtomic(..), SfxAtomic(..), HitAtomic(..)+ , undoCmdAtomic, undoSfxAtomic, undoAtomic+ ) where++import Data.Binary+import Data.Text (Text)+import GHC.Generics (Generic)++import Game.LambdaHack.Common.Actor+import qualified Game.LambdaHack.Common.Color as Color+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.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.Point+import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.ItemKind as ItemKind+import Game.LambdaHack.Content.TileKind as TileKind++data Atomic =+ CmdAtomic CmdAtomic+ | SfxAtomic SfxAtomic+ deriving (Show, Eq, Generic)++instance Binary Atomic++-- | Abstract syntax of atomic commands.+data CmdAtomic =+ -- Create/destroy actors and items.+ CreateActorA ActorId Actor [(ItemId, Item)]+ | DestroyActorA ActorId Actor [(ItemId, Item)]+ | CreateItemA ItemId Item Int Container+ | DestroyItemA ItemId Item Int Container+ | SpotActorA ActorId Actor [(ItemId, Item)]+ | LoseActorA ActorId Actor [(ItemId, Item)]+ | SpotItemA ItemId Item Int Container+ | LoseItemA ItemId Item Int Container+ -- Move actors and items.+ | MoveActorA ActorId Point Point+ | WaitActorA ActorId Time Time+ | 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])+ | ColorActorA ActorId (Maybe Color.Color) (Maybe Color.Color)+ -- Change faction attributes.+ | QuitFactionA FactionId (Maybe Actor) (Maybe Status) (Maybe Status)+ | LeadFactionA FactionId (Maybe ActorId) (Maybe ActorId)+ | DiplFactionA FactionId FactionId Diplomacy Diplomacy+ -- Alter map.+ | AlterTileA LevelId Point (Kind.Id TileKind) (Kind.Id TileKind)+ | SearchTileA ActorId Point (Kind.Id TileKind) (Kind.Id TileKind)+ | SpotTileA LevelId [(Point, Kind.Id TileKind)]+ | LoseTileA LevelId [(Point, Kind.Id TileKind)]+ | AlterSmellA LevelId Point (Maybe Time) (Maybe Time)+ | SpotSmellA LevelId [(Point, Time)]+ | LoseSmellA LevelId [(Point, Time)]+ -- Assorted.+ | AgeLevelA LevelId Time+ | AgeGameA Time+ | DiscoverA LevelId Point ItemId (Kind.Id ItemKind)+ | CoverA LevelId Point ItemId (Kind.Id ItemKind)+ | PerceptionA LevelId PerActor PerActor+ | RestartA FactionId Discovery FactionPers State Bool Text+ | RestartServerA State+ | ResumeA FactionId FactionPers+ | ResumeServerA State+ | KillExitA FactionId+ | SaveExitA+ | SaveBkpA+ | MsgAllA Msg+ deriving (Show, Eq, Generic)++instance Binary CmdAtomic++data SfxAtomic =+ StrikeD ActorId ActorId Item HitAtomic+ | RecoilD ActorId ActorId Item HitAtomic+ | ProjectD ActorId ItemId+ | CatchD ActorId ItemId+ | ActivateD ActorId ItemId+ | CheckD ActorId ItemId+ | TriggerD ActorId Point F.Feature Bool+ | ShunD ActorId Point F.Feature Bool+ | EffectD ActorId (Effect.Effect Int)+ | MsgFidD FactionId Msg+ | MsgAllD Msg+ | DisplayPushD FactionId+ | DisplayDelayD FactionId+ deriving (Show, Eq, Generic)++instance Binary SfxAtomic++data HitAtomic = HitD | HitBlockD | MissBlockD+ deriving (Show, Eq, Generic)++instance Binary HitAtomic++undoCmdAtomic :: CmdAtomic -> Maybe CmdAtomic+undoCmdAtomic cmd = case cmd of+ CreateActorA aid body ais -> Just $ DestroyActorA aid body ais+ DestroyActorA aid body ais -> Just $ CreateActorA aid body ais+ CreateItemA iid item k c -> Just $ DestroyItemA iid item k c+ DestroyItemA iid item k c -> Just $ CreateItemA iid item k c+ SpotActorA aid body ais -> Just $ LoseActorA aid body ais+ LoseActorA aid body ais -> Just $ SpotActorA aid body ais+ SpotItemA iid item k c -> Just $ LoseItemA iid item k c+ LoseItemA iid item k c -> Just $ SpotItemA iid item k c+ MoveActorA aid fromP toP -> Just $ MoveActorA aid toP fromP+ WaitActorA aid fromWait toWait -> Just $ WaitActorA aid toWait fromWait+ DisplaceActorA source target -> Just $ DisplaceActorA target source+ MoveItemA iid k c1 c2 ->Just $ MoveItemA iid k c2 c1+ 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+ 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+ DiplFactionA fid1 fid2 fromDipl toDipl ->+ Just $ DiplFactionA fid1 fid2 toDipl fromDipl+ AlterTileA lid p fromTile toTile -> Just $ AlterTileA lid p toTile fromTile+ SearchTileA aid p fromTile toTile -> Just $ SearchTileA aid p toTile fromTile+ SpotTileA lid ts -> Just $ LoseTileA lid ts+ LoseTileA lid ts -> Just $ SpotTileA lid ts+ AlterSmellA lid p fromSm toSm -> Just $ AlterSmellA lid p toSm fromSm+ SpotSmellA lid sms -> Just $ LoseSmellA lid sms+ LoseSmellA lid sms -> Just $ SpotSmellA lid sms+ AgeLevelA lid t -> Just $ AgeLevelA lid (timeNegate t)+ AgeGameA t -> Just $ AgeGameA (timeNegate t)+ DiscoverA lid p iid ik -> Just $ CoverA lid p iid ik+ CoverA lid p iid ik -> Just $ DiscoverA lid p iid ik+ PerceptionA lid outPer inPer -> Just $ PerceptionA lid inPer outPer+ RestartA{} -> Just cmd -- here history ends; change direction+ RestartServerA{} -> Just cmd -- here history ends; change direction+ ResumeA{} -> Nothing+ ResumeServerA{} -> Nothing+ KillExitA{} -> Nothing+ SaveExitA -> Nothing+ SaveBkpA -> Nothing+ MsgAllA{} -> Nothing -- only generated by @cmdAtomicFilterCli@++undoSfxAtomic :: SfxAtomic -> SfxAtomic+undoSfxAtomic cmd = case cmd of+ StrikeD source target item b -> RecoilD source target item b+ RecoilD source target item b -> StrikeD source target item b+ ProjectD aid iid -> CatchD aid iid+ CatchD aid iid -> ProjectD aid iid+ ActivateD aid iid -> CheckD aid iid+ CheckD aid iid -> ActivateD aid iid+ TriggerD aid p feat b -> ShunD aid p feat b+ ShunD aid p feat b -> TriggerD aid p feat b+ EffectD{} -> cmd -- not ideal?+ MsgFidD{} -> cmd+ MsgAllD{} -> cmd+ DisplayPushD{} -> cmd+ DisplayDelayD{} -> cmd++undoAtomic :: Atomic -> Maybe Atomic+undoAtomic (CmdAtomic cmd) = fmap CmdAtomic $ undoCmdAtomic cmd+undoAtomic (SfxAtomic sfx) = Just $ SfxAtomic $ undoSfxAtomic sfx
+ Game/LambdaHack/Common/AtomicPos.hs view
@@ -0,0 +1,247 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Semantics of atomic commands shared by client and server.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Common.AtomicPos+ ( PosAtomic(..), posCmdAtomic, posSfxAtomic+ , resetsFovAtomic, breakCmdAtomic, loudCmdAtomic+ , seenAtomicCli, seenAtomicSer+ ) where++import qualified Data.EnumSet as ES+import Data.Text (Text)++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.AtomicSem (posOfAid, posOfContainer)+import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.Perception+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Utils.Assert++-- All functions here that take an atomic action are executed+-- in the state just before the action is executed.++-- | The type representing visibility of actions to factions,+-- based on the position of the action, etc.+data PosAtomic =+ PosSight LevelId [Point] -- ^ whomever sees all the positions, notices+ | PosFidAndSight FactionId LevelId [Point]+ -- ^ observers and the faction notice+ | PosSmell LevelId [Point] -- ^ whomever smells all the positions, notices+ | PosFid FactionId -- ^ only the faction notices+ | PosFidAndSer FactionId -- ^ faction and server notices+ | PosSer -- ^ only the server notices+ | PosAll -- ^ everybody notices+ | PosNone -- ^ never broadcasted, but sent manually+ deriving (Show, Eq)++-- | Produces the positions where the action takes place. If a faction+-- is returned, the action is visible only for that faction, if Nothing+-- is returned, it's never visible. Empty list of positions implies+-- the action is visible always.+--+-- The goal of the mechanics: client should not get significantly+-- more information by looking at the atomic commands he is able to see+-- than by looking at the state changes they enact. E.g., @DisplaceActorA@+-- in a black room, with one actor carrying a 0-radius light would not be+-- distinguishable by looking at the state (or the screen) from @MoveActorA@+-- of the illuminated actor, hence such @DisplaceActorA@ should not be+-- observable, but @MoveActorA@ should be (or the former should be perceived+-- as the latter). However, to simplify, we assing as strict visibility+-- requirements to @MoveActorA@ as to @DisplaceActorA@ and fall back+-- to @SpotActorA@ (which provides minimal information that does not+-- 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]+ 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]+ SpotItemA _ _ _ c -> singleContainer c+ LoseItemA _ _ _ c -> singleContainer c+ MoveActorA aid fromP toP -> do+ (lid, _) <- posOfAid aid+ 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]+ 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]+ AgeActorA aid _ -> singleAid aid+ HealActorA aid _ -> singleAid aid+ HasteActorA aid _ -> singleAid aid+ PathActorA aid _ _ -> singleAid aid+ ColorActorA aid _ _ -> singleAid aid+ QuitFactionA{} -> return PosAll+ LeadFactionA fid _ _ -> return $ PosFidAndSer fid+ DiplFactionA{} -> return PosAll+ AlterTileA lid p _ _ -> return $ PosSight lid [p]+ SearchTileA aid p _ _ -> do+ (lid, pos) <- posOfAid aid+ return $ PosSight lid [pos, p]+ SpotTileA lid ts -> do+ let ps = map fst ts+ return $ PosSight lid ps+ LoseTileA lid ts -> do+ let ps = map fst ts+ return $ PosSight lid ps+ AlterSmellA lid p _ _ -> return $ PosSmell lid [p]+ SpotSmellA lid sms -> do+ let ps = map fst sms+ return $ PosSmell lid ps+ LoseSmellA lid sms -> do+ let ps = map fst sms+ 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]+ PerceptionA{} -> return PosNone+ RestartA fid _ _ _ _ _ -> return $ PosFid fid+ RestartServerA _ -> return PosSer+ ResumeA fid _ -> return $ PosFid fid+ ResumeServerA _ -> return PosSer+ KillExitA fid -> return $ PosFid fid+ SaveExitA -> return PosAll+ SaveBkpA -> return PosAll+ MsgAllA{} -> return PosAll++posSfxAtomic :: MonadActionRO m => SfxAtomic -> m PosAtomic+posSfxAtomic cmd = case cmd of+ StrikeD source target _ _ -> do+ (slid, sp) <- posOfAid source+ (tlid, tp) <- posOfAid target+ 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]+ 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]+ ShunD aid p _ _ -> do+ (lid, pa) <- posOfAid aid+ return $ PosSight lid [pa, p]+ EffectD aid _ -> singleAid aid+ MsgFidD fid _ -> return $ PosFid fid+ MsgAllD _ -> return PosAll+ DisplayPushD fid -> return $ PosFid fid+ DisplayDelayD fid -> return $ PosFid fid++singleAid :: MonadActionRO m => ActorId -> m PosAtomic+singleAid aid = do+ b <- getsState $ getActorBody aid+ return $ PosFidAndSight (bfid b) (blid b) [bpos b]++singleContainer :: MonadActionRO m => Container -> m PosAtomic+singleContainer c = do+ (lid, p) <- posOfContainer c+ 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)+-- of that faction does not change upon recomputation. Otherwise,+-- save/restore would change game state.+resetsFovAtomic :: MonadActionRO m => CmdAtomic -> m (Maybe [FactionId])+resetsFovAtomic cmd = case cmd of+ CreateActorA _ body _ -> return $ Just [bfid body]+ DestroyActorA _ body _ -> return $ Just [bfid body]+ SpotActorA _ body _ -> return $ Just [bfid body]+ LoseActorA _ body _ -> return $ Just [bfid body]+ CreateItemA{} -> return $ Just [] -- unless shines+ DestroyItemA{} -> return $ Just [] -- ditto+ MoveActorA aid _ _ -> fmap Just $ fidOfAid aid -- assumption: has no light+-- TODO: MoveActorCarryingLIghtA _ _ _ -> return Nothing+ DisplaceActorA source target -> do+ sfid <- fidOfAid source+ tfid <- fidOfAid target+ return $ Just $ if source == target+ then []+ else sfid ++ tfid+ MoveItemA{} -> return $ Just [] -- unless shiny+ AlterTileA{} -> return Nothing -- even if pos not visible initially+ _ -> return $ Just []++fidOfAid :: MonadActionRO m => ActorId -> m [FactionId]+fidOfAid aid = getsState $ (: []) . bfid . getActorBody aid++-- | Decompose an atomic action. The original action is visible+-- if it's positions are visible both before and after the action+-- (in between the FOV might have changed). The decomposed actions+-- are only tested vs the FOV after the action and they give reduced+-- information that still modifies client's state to match the server state+-- wrt the current FOV and the subset of @posCmdAtomic@ that is visible.+-- The original actions give more information not only due to spanning+-- potentially more positions than those visible. E.g., @MoveActorA@+-- informs about the continued existence of the actor between+-- moves, v.s., popping out of existence and then back in.+breakCmdAtomic :: MonadActionRO m => CmdAtomic -> m [CmdAtomic]+breakCmdAtomic cmd = case cmd of+ MoveActorA aid _ toP -> do+ b <- getsState $ getActorBody aid+ ais <- getsState $ getActorItem aid+ return [ LoseActorA aid b ais+ , SpotActorA aid b {bpos = toP, boldpos = bpos b} ais ]+ DisplaceActorA source target -> do+ sb <- getsState $ getActorBody source+ sais <- getsState $ getActorItem source+ tb <- getsState $ getActorBody target+ tais <- getsState $ getActorItem target+ return [ LoseActorA source sb sais+ , SpotActorA source sb {bpos = bpos tb, boldpos = bpos sb} sais+ , LoseActorA target tb tais+ , SpotActorA target tb {bpos = bpos sb, boldpos = bpos tb} tais+ ]+ MoveItemA iid k c1 c2 -> do+ item <- getsState $ getItemBody iid+ return [LoseItemA iid item k c1, SpotItemA iid item k c2]+ _ -> return [cmd]++loudCmdAtomic :: FactionId -> CmdAtomic -> Bool+loudCmdAtomic fid cmd = case cmd of+ DestroyActorA _ body _ ->+ -- Death of a party member does not need to be heard, because it's seen.+ not $ fid == bfid body || bproj body+ _ -> False++seenAtomicCli :: Bool -> FactionId -> Perception -> PosAtomic -> Bool+seenAtomicCli knowEvents fid per posAtomic =+ case posAtomic of+ PosSight _ ps -> knowEvents || all (`ES.member` totalVisible per) ps+ PosFidAndSight fid2 _ ps ->+ knowEvents || fid == fid2 || all (`ES.member` totalVisible per) ps+ PosSmell _ ps -> knowEvents || all (`ES.member` smellVisible per) ps+ PosFid fid2 -> fid == fid2+ PosFidAndSer fid2 -> fid == fid2+ PosSer -> False+ PosAll -> True+ PosNone -> assert `failure` fid++seenAtomicSer :: PosAtomic -> Bool+seenAtomicSer posAtomic =+ case posAtomic of+ PosFid _ -> False+ PosNone -> assert `failure` ("PosNone considered for the server" :: Text)+ _ -> True
+ Game/LambdaHack/Common/AtomicSem.hs view
@@ -0,0 +1,409 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Semantics of atomic commands shared by client and server.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Common.AtomicSem+ ( cmdAtomicSem+ , posOfAid, posOfContainer+ ) where++import Control.Arrow (second)+import Control.Monad+import qualified Data.EnumMap.Strict as EM+import Data.List+import Data.Maybe++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import qualified Game.LambdaHack.Common.Color as Color+import Game.LambdaHack.Common.Faction+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.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.TileKind as TileKind+import Game.LambdaHack.Utils.Assert++cmdAtomicSem :: MonadAction m => CmdAtomic -> m ()+cmdAtomicSem cmd = case cmd of+ CreateActorA aid body ais -> createActorA aid body ais+ DestroyActorA aid body ais -> destroyActorA aid body ais+ CreateItemA iid item k c -> createItemA iid item k c+ DestroyItemA iid item k c -> destroyItemA iid item k c+ SpotActorA aid body ais -> createActorA aid body ais+ LoseActorA aid body ais -> destroyActorA aid body ais+ SpotItemA iid item k c -> createItemA iid item k c+ LoseItemA iid item k c -> destroyItemA iid item k c+ MoveActorA aid fromP toP -> moveActorA aid fromP toP+ WaitActorA aid fromWait toWait -> waitActorA aid fromWait toWait+ DisplaceActorA source target -> displaceActorA source target+ MoveItemA iid k c1 c2 -> moveItemA iid k c1 c2+ 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+ 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+ DiplFactionA fid1 fid2 fromDipl toDipl ->+ diplFactionA fid1 fid2 fromDipl toDipl+ AlterTileA lid p fromTile toTile -> alterTileA lid p fromTile toTile+ SearchTileA _ _ fromTile toTile ->+ assert (fromTile /= toTile) $ return () -- only for clients+ SpotTileA lid ts -> spotTileA lid ts+ LoseTileA lid ts -> loseTileA lid ts+ AlterSmellA lid p fromSm toSm -> alterSmellA lid p fromSm toSm+ SpotSmellA lid sms -> spotSmellA lid sms+ LoseSmellA lid sms -> loseSmellA lid sms+ AgeLevelA lid t -> ageLevelA lid t+ 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 ()+ RestartA fid sdisco sfper s _ _ -> restartA fid sdisco sfper s+ RestartServerA s -> restartServerA s+ ResumeA{} -> return ()+ ResumeServerA s -> resumeServerA s+ KillExitA{} -> return ()+ SaveExitA -> return ()+ SaveBkpA -> return ()+ MsgAllA{} -> return ()++-- | Creates an actor. Note: after this command, usually a new leader+-- for the party should be elected (in case this actor is the only one alive).+createActorA :: MonadAction m => ActorId -> Actor -> [(ItemId, Item)] -> m ()+createActorA aid body ais = do+ -- Add actor to @sactorD@.+ let f Nothing = Just body+ f (Just b) = assert `failure` (aid, body, b)+ modifyState $ updateActorD $ EM.alter f aid+ -- Add actor to @sprio@.+ let g Nothing = Just [aid]+ g (Just l) = assert (aid `notElem` l `blame` (aid, body, l))+ $ Just $ aid : l+ updateLevel (blid body) $ updatePrio $ EM.alter g (btime body)+ -- Actor's items may or may not be already present in @sitemD@,+ -- regardless if they are already present otherwise in the dungeon.+ -- We re-add them all to save time determining which really need it.+ forM_ ais $ \(iid, item) -> do+ let h item1 item2 =+ assert (item1 == item2 `blame` (aid, body, iid, item1, item2)) item1+ modifyState $ updateItemD $ EM.insertWith h iid item++-- | Update a given level data within state.+updateLevel :: MonadAction m => LevelId -> (Level -> Level) -> m ()+updateLevel lid f = modifyState $ updateDungeon $ EM.adjust f lid++-- | Kills an actor. Note: after this command, usually a new leader+-- for the party should be elected.+destroyActorA :: MonadAction m => ActorId -> Actor -> [(ItemId, Item)] -> m ()+destroyActorA aid body ais = do+ -- Assert that actor's items belong to @sitemD@. Do not remove those+ -- that do not appear anywhere else, for simplicity and speed.+ itemD <- getsState sitemD+ let match (iid, item) = itemD EM.! iid == item+ assert (allB match ais `blame` (aid, body, ais, itemD)) skip+ -- Remove actor from @sactorD@.+ let f Nothing = assert `failure` (aid, body)+ f (Just b) = assert (b == body `blame` (aid, body, b)) Nothing+ modifyState $ updateActorD $ EM.alter f aid+ -- Remove actor from @sprio@.+ let g Nothing = assert `failure` (aid, body)+ g (Just l) = assert (aid `elem` l `blame` (aid, body, l))+ $ let l2 = delete aid l+ in if null l2 then Nothing else Just l2+ updateLevel (blid body) $ updatePrio $ EM.alter g (btime body)++-- | Create a few copies of an item that is already registered for the dungeon+-- (in @sitemRev@ field of @StateServer@).+createItemA :: MonadAction m => ItemId -> Item -> Int -> Container -> m ()+createItemA iid item k c = assert (k > 0) $ do+ -- The item may or may not be already present in @sitemD@,+ -- regardless if it's actually present in the dungeon.+ let f item1 item2 = assert (item1 == item2 `blame` (iid, item, k, c)) item1+ modifyState $ updateItemD $ EM.insertWith f iid item+ case c of+ CFloor lid pos -> insertItemFloor lid iid k pos+ CActor aid l -> insertItemActor iid k l aid++insertItemFloor :: MonadAction m+ => LevelId -> ItemId -> Int -> Point -> m ()+insertItemFloor lid iid k pos =+ let bag = EM.singleton iid k+ mergeBag = EM.insertWith (EM.unionWith (+)) pos bag+ in updateLevel lid $ updateFloor mergeBag++insertItemActor :: MonadAction m+ => ItemId -> Int -> InvChar -> ActorId -> m ()+insertItemActor iid k l aid = do+ let bag = EM.singleton iid k+ upd = EM.unionWith (+) bag+ modifyState $ updateActorD $+ EM.adjust (\b -> b {bbag = upd (bbag b)}) aid+ modifyState $ updateActorD $+ EM.adjust (\b -> b {binv = EM.insert l iid (binv b)}) aid+ modifyState $ updateActorBody aid $ \b ->+ b {bletter = max l (bletter b)}++-- | Destroy some copies (possibly not all) of an item.+destroyItemA :: MonadAction m => ItemId -> Item -> Int -> Container -> m ()+destroyItemA iid item k c = assert (k > 0) $ do+ -- Do not remove the item from @sitemD@ nor from @sitemRev@,+ -- It's incredibly costly and not noticeable for the player.+ -- However, assert the item is registered in @sitemD@.+ itemD <- getsState sitemD+ assert (iid `EM.lookup` itemD == Just item `blame` (iid, item, itemD)) skip+ case c of+ CFloor lid pos -> deleteItemFloor lid iid k pos+ CActor aid l -> deleteItemActor iid k l aid++deleteItemFloor :: MonadAction m+ => LevelId -> ItemId -> Int -> Point -> m ()+deleteItemFloor lid iid k pos =+ let rmFromFloor (Just bag) =+ let nbag = rmFromBag k iid bag+ in if EM.null nbag then Nothing else Just nbag+ rmFromFloor Nothing = assert `failure` (lid, iid, k, pos)+ in updateLevel lid $ updateFloor $ EM.alter rmFromFloor pos++deleteItemActor :: MonadAction m+ => ItemId -> Int -> InvChar -> ActorId -> m ()+deleteItemActor iid k l aid = do+ modifyState $ updateActorD $+ EM.adjust (\b -> b {bbag = rmFromBag k iid (bbag b)}) aid+ -- Do not remove from actor's @binv@, but assert it was there.+ b <- getsState $ getActorBody aid+ assert (l `EM.lookup` binv b == Just iid `blame` (iid, l, aid)) skip+ -- Actor's @bletter@ for UI not reset, but checked.+ assert (bletter b >= l`blame` (iid, k, l, aid, bletter b)) skip++moveActorA :: MonadAction m => ActorId -> Point -> Point -> m ()+moveActorA aid fromP toP = assert (fromP /= toP) $ do+ b <- getsState $ getActorBody aid+ assert (fromP == bpos b `blame` (aid, fromP, toP, bpos b, b)) skip+ modifyState $ updateActorBody aid+ $ \body -> body {bpos = toP, boldpos = fromP}++waitActorA :: MonadAction m => ActorId -> Time -> Time -> m ()+waitActorA aid fromWait toWait = assert (fromWait /= toWait) $ do+ b <- getsState $ getActorBody aid+ assert (fromWait == bwait b `blame` (aid, fromWait, toWait, bwait b, b)) skip+ modifyState $ updateActorBody aid $ \body -> body {bwait = toWait}++displaceActorA :: MonadAction m => ActorId -> ActorId -> m ()+displaceActorA source target = assert (source /= target) $ do+ spos <- getsState $ bpos . getActorBody source+ tpos <- getsState $ bpos . getActorBody target+ modifyState $ updateActorBody source $ \ b -> b {bpos = tpos, boldpos = spos}+ modifyState $ updateActorBody target $ \ b -> b {bpos = spos, boldpos = tpos}++moveItemA :: MonadAction m => ItemId -> Int -> Container -> Container -> m ()+moveItemA iid k c1 c2 = assert (k > 0 && c1 /= c2) $ do+ (lid1, _) <- posOfContainer c1+ (lid2, _) <- posOfContainer c2+ assert (lid1 == lid2 `blame` (iid, k, c1, c2, lid1, lid2)) skip+ case c1 of+ CFloor lid pos -> deleteItemFloor lid iid k pos+ CActor aid l -> deleteItemActor iid k l aid+ case c2 of+ CFloor lid pos -> insertItemFloor lid iid k pos+ CActor aid l -> insertItemActor iid k l aid++posOfAid :: MonadActionRO m => ActorId -> m (LevelId, Point)+posOfAid aid = do+ b <- getsState $ getActorBody aid+ return (blid b, bpos b)++posOfContainer :: MonadActionRO m => Container -> m (LevelId, Point)+posOfContainer (CFloor lid p) = return (lid, p)+posOfContainer (CActor aid _) = posOfAid aid++-- TODO: optimize (a single call to updatePrio is enough)+ageActorA :: MonadAction m => ActorId -> Time -> m ()+ageActorA aid t = assert (t /= timeZero) $ do+ body <- getsState $ getActorBody aid+ ais <- getsState $ getActorItem aid+ destroyActorA aid body ais+ createActorA aid body {btime = timeAdd (btime body) t} ais++healActorA :: MonadAction m => ActorId -> Int -> m ()+healActorA aid n = assert (n /= 0) $+ modifyState $ updateActorBody aid $ \b -> b {bhp = n + bhp b}++hasteActorA :: MonadAction m => ActorId -> Speed -> m ()+hasteActorA aid delta = assert (delta /= speedZero) $ do+ Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ modifyState $ updateActorBody aid $ \ b ->+ let innateSpeed = aspeed $ okind $ bkind b+ curSpeed = fromMaybe innateSpeed (bspeed b)+ newSpeed = speedAdd curSpeed delta+ in assert (newSpeed >= speedZero `blame` (aid, curSpeed, delta)) $+ if newSpeed == innateSpeed+ then b {bspeed = Nothing}+ else b {bspeed = Just newSpeed}++pathActorA :: MonadAction m+ => ActorId -> Maybe [Vector] -> Maybe [Vector] -> m ()+pathActorA aid fromPath toPath = assert (fromPath /= toPath) $ do+ body <- getsState $ getActorBody aid+ assert (fromPath == bpath body `blame` (aid, fromPath, toPath, body)) skip+ modifyState $ updateActorBody aid $ \b -> b {bpath = toPath}++colorActorA :: MonadAction m+ => ActorId -> Maybe Color.Color -> Maybe Color.Color -> m ()+colorActorA aid fromCol toCol = assert (fromCol /= toCol) $ do+ body <- getsState $ getActorBody aid+ assert (fromCol == bcolor body `blame` (aid, fromCol, toCol, body)) skip+ modifyState $ updateActorBody aid $ \b -> b {bcolor = toCol}++quitFactionA :: MonadAction m+ => FactionId -> Maybe Actor -> Maybe Status -> Maybe Status+ -> m ()+quitFactionA fid mbody fromSt toSt = assert (fromSt /= toSt) $ do+ assert (maybe True ((fid ==) . bfid) mbody) skip+ fact <- getsState $ (EM.! fid) . sfactionD+ assert (fromSt == gquit fact `blame` (fid, fromSt, toSt, fact)) skip+ let adj fa = fa {gquit = toSt}+ modifyState $ updateFaction $ EM.adjust adj fid++-- The previous leader is assumed to be alive.+leadFactionA :: MonadAction m+ => FactionId -> Maybe ActorId -> Maybe ActorId -> m ()+leadFactionA fid source target = assert (source /= target) $ do+ fact <- getsState $ (EM.! fid) . sfactionD+ assert (source == gleader fact `blame` (fid, source, target, fact)) skip+ let adj fa = fa {gleader = target}+ modifyState $ updateFaction $ EM.adjust adj fid++diplFactionA :: MonadAction m+ => FactionId -> FactionId -> Diplomacy -> Diplomacy -> m ()+diplFactionA fid1 fid2 fromDipl toDipl =+ assert (fid1 /= fid2 && fromDipl /= toDipl) $ do+ fact1 <- getsState $ (EM.! fid1) . sfactionD+ fact2 <- getsState $ (EM.! fid2) . sfactionD+ assert (fromDipl == EM.findWithDefault Unknown fid2 (gdipl fact1)+ && fromDipl == EM.findWithDefault Unknown fid1 (gdipl fact2)+ `blame` (fid1, fid2, fromDipl, toDipl, fact1, fact2)) skip+ let adj fid fact = fact {gdipl = EM.insert fid toDipl (gdipl fact)}+ modifyState $ updateFaction $ EM.adjust (adj fid2) fid1+ 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@.+alterTileA :: MonadAction m+ => LevelId -> Point -> Kind.Id TileKind -> Kind.Id TileKind+ -> m ()+alterTileA lid p fromTile toTile = assert (fromTile /= toTile) $ do+ Kind.COps{cotile} <- getsState scops+ freshClientTile <- getsLevel lid $ hideTile cotile p+ -- The second alternative can happen if, e.g., a client remembers,+ -- but does not see the tile (so does not notice the SearchTileA action),+ -- 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+ `blame` (lid, p, fromTile, toTile, ts Kind.! p))+ $ ts Kind.// [(p, toTile)]+ updateLevel lid $ updateTile adj+ case (Tile.isExplorable cotile fromTile, Tile.isExplorable cotile toTile) of+ (False, True) -> updateLevel lid $ \lvl -> lvl {lseen = lseen lvl + 1}+ (True, False) -> updateLevel lid $ \lvl -> lvl {lseen = lseen lvl - 1}+ _ -> return ()++-- Notice a 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+-- at previously seen positions. Similarly, when updating the @lseen@+-- field we don't assume the tiles were unknown previously.+spotTileA :: MonadAction m => LevelId -> [(Point, Kind.Id TileKind)] -> m ()+spotTileA lid ts = assert (not $ null ts) $ do+ Kind.COps{cotile} <- getsState scops+ tileM <- getsLevel lid ltile+ let adj tileMap = tileMap Kind.// ts+ updateLevel lid $ updateTile adj+ let f (p, t2) = do+ let t1 = tileM Kind.! 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 invisible 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+ Kind.COps{cotile=cotile@Kind.Ops{ouniqGroup}} <- getsState scops+ let unknownId = ouniqGroup "unknown space"+ matches _ [] = True+ matches tileMap ((p, ov) : rest) =+ tileMap Kind.! p == ov && matches tileMap rest+ tu = map (second (const unknownId)) ts+ adj tileMap = assert (matches tileMap ts) $ tileMap Kind.// tu+ updateLevel lid $ updateTile adj+ let f (_, t1) =+ when (Tile.isExplorable cotile t1) $+ updateLevel lid $ \lvl -> lvl {lseen = lseen lvl - 1}+ mapM_ f ts++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` (lid, p, fromSm, toSm, sm)) toSm+ let alt _ = 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` (lid, sms, sm, oldSm)+ f (p, sm) = EM.alter (alt sm) p+ upd m = foldr f m sms+ updateLevel lid $ updateSmell upd++loseSmellA :: MonadAction m => LevelId -> [(Point, Time)] -> m ()+loseSmellA lid sms = assert (not $ null sms) $ do+ let alt sm Nothing = assert `failure` (lid, sms, sm)+ alt sm (Just oldSm) =+ assert (sm == oldSm `blame` (lid, sms, sm, oldSm)) Nothing+ f (p, sm) = EM.alter (alt sm) p+ upd m = foldr f m sms+ updateLevel lid $ updateSmell upd++-- | Age the level.+--+-- Not aging the game here, since not all factions see the level,+-- so not all get this command (it would lead information that+-- there is somebody's leader on the level).+ageLevelA :: MonadAction m => LevelId -> Time -> m ()+ageLevelA lid delta = assert (delta /= timeZero) $+ updateLevel lid $ \lvl -> lvl {ltime = timeAdd (ltime lvl) delta}++ageGameA :: MonadAction m => Time -> m ()+ageGameA delta = assert (delta /= timeZero) $+ modifyState $ updateTime $ timeAdd delta++restartA :: MonadAction m+ => FactionId -> Discovery -> FactionPers -> State -> m ()+restartA _ _ _ = putState++restartServerA :: MonadAction m => State -> m ()+restartServerA = putState++resumeServerA :: MonadAction m => State -> m ()+resumeServerA = putState
+ Game/LambdaHack/Common/ClientCmd.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}+-- | Abstract syntax of client commands.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Common.ClientCmd+ ( CmdClientAI(..), CmdClientUI(..)+ , debugCmdClientAI, debugCmdClientUI, debugAid+ , ChanServer(..), ConnServerFaction, ConnServerDict+ ) where++import Control.Concurrent.STM.TQueue+import qualified Data.EnumMap.Strict as EM+import Data.Text (Text)++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.AtomicPos+import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.ServerCmd+import Game.LambdaHack.Common.State+import Game.LambdaHack.Frontend++-- | Abstract syntax of client commands that don't use the UI.+data CmdClientAI =+ CmdAtomicAI CmdAtomic+ | CmdQueryAI ActorId+ deriving Show++-- | Abstract syntax of client commands that use the UI.+data CmdClientUI =+ CmdAtomicUI CmdAtomic+ | SfxAtomicUI SfxAtomic+ | CmdQueryUI ActorId+ deriving Show++debugCmdClientAI :: MonadActionRO m => CmdClientAI -> m Text+debugCmdClientAI cmd = case cmd of+ CmdAtomicAI cmdA -> do+ ps <- posCmdAtomic cmdA+ return $ showT (showT cmd, ps)+ CmdQueryAI aid -> debugAid aid $ showT cmd++debugCmdClientUI :: MonadActionRO m => CmdClientUI -> m Text+debugCmdClientUI cmd = case cmd of+ CmdAtomicUI cmdA -> do+ ps <- posCmdAtomic cmdA+ return $ showT (showT cmd, ps)+ SfxAtomicUI sfx -> do+ ps <- posSfxAtomic sfx+ return $ showT (showT cmd, ps)+ CmdQueryUI aid -> debugAid aid $ showT cmd++debugAid :: MonadActionRO m => ActorId -> Text -> m Text+debugAid aid s = do+ b <- getsState $ getActorBody aid+ time <- getsState $ getLocalTime (blid b)+ return $+ showT (s, "lid", blid b, "time", time, "aid", aid, "faction", bfid b)++-- | Connection channels between the server and a single client.+data ChanServer c = ChanServer+ { fromServer :: TQueue c+ , toServer :: TQueue CmdSer+ }++instance Show (ChanServer c) where+ show _ = "client-server connection channels"++-- | Connection to the human-controlled client of a faction and/or+-- to the AI client for the same faction.+type ConnServerFaction = ( (ChanFrontend, ChanServer CmdClientUI)+ , ChanServer CmdClientAI )++-- | Connection information for all factions, indexed by faction identifier.+type ConnServerDict = EM.EnumMap FactionId ConnServerFaction
+ Game/LambdaHack/Common/Color.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE DeriveGeneric #-}+-- | Colours and text attributes.+module Game.LambdaHack.Common.Color+ ( -- * Colours+ Color(..), defBG, defFG, isBright, legalBG, darkCol, brightCol, stdCol+ , colorToRGB+ -- * Text attributes and the screen+ , Attr(..), defAttr, AttrChar(..)+ ) where++import Data.Binary+import qualified Data.Hashable as Hashable+import GHC.Generics (Generic)++-- TODO: since this type may be essential to speed, consider implementing+-- it as an Int, with color numbered as they are on terminals, see+-- http://www.haskell.org/haskellwiki/Performance/Data_types#Enumerations+-- If we ever switch to 256 colours, the Int implementation or similar+-- will be more natural, anyway.+-- | Colours supported by the major frontends.+data Color =+ Black+ | Red+ | Green+ | Brown+ | Blue+ | Magenta+ | Cyan+ | White+ | BrBlack+ | BrRed+ | BrGreen+ | BrYellow+ | BrBlue+ | BrMagenta+ | BrCyan+ | BrWhite+ deriving (Show, Eq, Ord, Enum, Bounded, Generic)++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+defFG = White++-- | Text attributes: foreground and backgroud colors.+data Attr = Attr+ { fg :: !Color -- ^ foreground colour+ , bg :: !Color -- ^ backgroud color+ }+ deriving (Show, Eq, Ord)++instance Binary Attr where+ put Attr{..} = do+ put fg+ put bg+ get = do+ fg <- get+ bg <- get+ return Attr{..}++-- | The default attribute, to optimize attribute setting.+defAttr :: Attr+defAttr = Attr defFG defBG++data AttrChar = AttrChar+ { acAttr :: !Attr+ , acChar :: !Char+ }+ deriving (Show, Eq)++instance Binary AttrChar where+ put AttrChar{..} = do+ put acAttr+ put acChar+ get = do+ acAttr <- get+ acChar <- get+ return AttrChar{..}++-- | A helper for the terminal frontends that display bright via bold.+isBright :: Color -> Bool+isBright c = c >= BrBlack++-- | Due to the limitation of the curses library used in the curses frontend,+-- only these are legal backgrounds.+legalBG :: [Color]+legalBG = [Black, White, Blue, Magenta]++-- | Colour sets.+darkCol, brightCol, stdCol :: [Color]+darkCol = [Red .. Cyan]+brightCol = [BrRed .. BrCyan] -- BrBlack is not really that bright+stdCol = darkCol ++ brightCol++-- | Translationg to heavily modified Linux console color RGB values.+colorToRGB :: Color -> String+colorToRGB Black = "#000000"+colorToRGB Red = "#D50000"+colorToRGB Green = "#00AA00"+colorToRGB Brown = "#AA5500"+colorToRGB Blue = "#203AF0"+colorToRGB Magenta = "#AA00AA"+colorToRGB Cyan = "#00AAAA"+colorToRGB White = "#C5BCB8"+colorToRGB BrBlack = "#6F5F5F"+colorToRGB BrRed = "#FF5555"+colorToRGB BrGreen = "#75FF45"+colorToRGB BrYellow = "#FFE855"+colorToRGB BrBlue = "#4090FF"+colorToRGB BrMagenta = "#FF77FF"+colorToRGB BrCyan = "#60FFF0"+colorToRGB BrWhite = "#FFFFFF"++-- | For reference, the original Linux console colors.+-- Good old retro feel and more useful than xterm (e.g. brown).+_olorToRGB :: Color -> String+_olorToRGB Black = "#000000"+_olorToRGB Red = "#AA0000"+_olorToRGB Green = "#00AA00"+_olorToRGB Brown = "#AA5500"+_olorToRGB Blue = "#0000AA"+_olorToRGB Magenta = "#AA00AA"+_olorToRGB Cyan = "#00AAAA"+_olorToRGB White = "#AAAAAA"+_olorToRGB BrBlack = "#555555"+_olorToRGB BrRed = "#FF5555"+_olorToRGB BrGreen = "#55FF55"+_olorToRGB BrYellow = "#FFFF55"+_olorToRGB BrBlue = "#5555FF"+_olorToRGB BrMagenta = "#FF55FF"+_olorToRGB BrCyan = "#55FFFF"+_olorToRGB BrWhite = "#FFFFFF"
+ Game/LambdaHack/Common/ContentDef.hs view
@@ -0,0 +1,23 @@+-- | A game requires the engine provided by the library, perhaps customized,+-- and game content, defined completely afresh for the particular game.+-- The general type of the content is @ContentDef@ and it has instances+-- for all content kinds, such as items kinds+-- (@Game.LambdaHack.Content.ItemKind@).+-- The possible kinds are fixed in the library and all defined in the same+-- directory. On the other hand, game content, that is all elements+-- of @ContentDef@ instances, are defined in a directory+-- of the game code proper, with names corresponding to their kinds.+module Game.LambdaHack.Common.ContentDef (ContentDef(..)) where++import Data.Text (Text)++import Game.LambdaHack.Common.Misc++-- | The general type of a particular game content, e.g., item kinds.+data ContentDef a = ContentDef+ { getSymbol :: a -> Char -- ^ symbol, e.g., to print on the map+ , getName :: a -> Text -- ^ name, e.g., to show to the player+ , getFreq :: a -> Freqs -- ^ frequency within groups+ , validate :: [a] -> [a] -- ^ validate and catch some offenders, if any+ , content :: [a] -- ^ all the defined content of this type+ }
+ Game/LambdaHack/Common/Effect.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE DeriveFunctor, DeriveGeneric, OverloadedStrings #-}+-- | Effects of content on other content. No operation in this module+-- involves the 'State' or 'Action' type.+module Game.LambdaHack.Common.Effect+ ( Effect(..), effectTrav, effectToSuffix, effectToBenefit+ ) where++import qualified Control.Monad.State as St+import Data.Binary+import qualified Data.Hashable as Hashable+import Data.Text (Text)+import GHC.Generics (Generic)++import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Utils.Assert++-- TODO: document each constructor+-- Effects of items, tiles, etc. The type argument represents power.+-- either as a random formula dependent on level, or as a final rolled value.+data Effect a =+ NoEffect+ | Heal Int+ | Hurt !RollDice a+ | Mindprobe Int -- the @Int@ is a hack to send the result to clients+ | Dominate+ | CallFriend Int+ | Summon Int+ | CreateItem Int+ | ApplyPerfume+ | Regeneration a+ | Searching a+ | Ascend Int+ | Descend Int+ | Escape+ deriving (Show, Read, Eq, Ord, Generic, Functor)++instance Hashable.Hashable a => Hashable.Hashable (Effect a)++instance Binary a => Binary (Effect a)++-- TODO: Traversable?+-- | 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 (Hurt dice a) f = do+ b <- f a+ 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 ApplyPerfume _ = return ApplyPerfume+effectTrav (Regeneration a) f = do+ b <- f a+ return $ Regeneration b+effectTrav (Searching a) f = do+ b <- f a+ return $ Searching b+effectTrav (Ascend p) _ = return $ Ascend p+effectTrav (Descend p) _ = return $ Descend p+effectTrav Escape _ = return Escape++-- | Suffix to append to a basic content name if the content causes the effect.+effectToSuff :: Effect a -> (a -> Text) -> Text+effectToSuff effect f =+ case St.evalState (effectTrav effect $ return . f) () of+ NoEffect -> ""+ Heal p | p > 0 -> "of healing" <> affixBonus p+ Heal 0 -> "of bloodletting"+ Heal p -> "of wounding" <> affixBonus p+ Hurt dice t -> "(" <> showT dice <> ")" <> t+ Mindprobe{} -> "of soul searching"+ Dominate -> "of domination"+ CallFriend p -> "of aid calling" <> affixPower p+ Summon p -> "of summoning" <> affixPower p+ CreateItem p -> "of item creation" <> affixPower p+ ApplyPerfume -> "of rose water"+ Regeneration t -> "of regeneration" <> t+ Searching t -> "of searching" <> t+ Ascend p -> "of ascending" <> affixPower p+ Descend p -> "of descending" <> affixPower p+ Escape -> "of escaping"++effectToSuffix :: Effect Int -> Text+effectToSuffix effect = effectToSuff effect affixBonus++affixPower :: Int -> Text+affixPower p = case compare p 1 of+ EQ -> ""+ LT -> assert `failure` p+ GT -> " (+" <> showT p <> ")"++affixBonus :: Int -> Text+affixBonus p = case compare p 0 of+ EQ -> ""+ LT -> " (" <> showT p <> ")"+ GT -> " (+" <> showT p <> ")"++-- | How much AI benefits from applying the effect. Multipllied by item p.+-- Negative means harm to the enemy when thrown at him. Effects with zero+-- benefit won't ever be used, neither actively nor passively.+effectToBenefit :: Effect Int -> Int+effectToBenefit NoEffect = 0+effectToBenefit (Heal p) = p * 10 -- TODO: depends on (maxhp - hp)+effectToBenefit (Hurt _ p) = -(p * 10) -- TODO: dice ignored for now+effectToBenefit Mindprobe{} = 0 -- AI can't benefit yet+effectToBenefit Dominate = 1 -- hard to use; TODO: limit by IQ+effectToBenefit (CallFriend p) = p * 100+effectToBenefit Summon{} = 5 -- may or may not spawn a friendly+effectToBenefit (CreateItem p) = p * 20+effectToBenefit ApplyPerfume = 0+effectToBenefit Regeneration{} = 0 -- bigger benefit from carrying around+effectToBenefit Searching{} = 0 -- AI doesn't search yet+effectToBenefit Ascend{} = 5 -- AI can't choose levels smartly yet+effectToBenefit Descend{} = 20 -- but it prefers to hide deep down+effectToBenefit Escape = 100 -- hero AI wants to win ASAP, monster+ -- AI sits on the exit to block it
+ Game/LambdaHack/Common/Faction.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Factions taking part in the game: e.g., two human players controlling+-- the hero faction battling the monster and the animal factions.+module Game.LambdaHack.Common.Faction+ ( FactionId, FactionDict, Faction(..), Diplomacy(..), Outcome(..), Status(..)+ , isHumanFact, usesAIFact, isSpawnFact, isSummonFact+ , isAtWar, isAllied+ ) where++import Data.Binary+import qualified Data.EnumMap.Strict as EM+import Data.Maybe+import Data.Text (Text)++import Game.LambdaHack.Common.Actor+import qualified Game.LambdaHack.Common.Color as Color+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Content.FactionKind+import Game.LambdaHack.Content.StrategyKind++-- | All factions in the game, indexed by faction identifier.+type FactionDict = EM.EnumMap FactionId Faction++data Faction = Faction+ { gkind :: !(Kind.Id FactionKind) -- ^ the kind of the faction+ , gname :: !Text -- ^ individual name+ , gconfig :: !Text -- ^ raw name specified in config+ , gAiLeader :: !(Maybe (Kind.Id StrategyKind))+ -- ^ AI for the leaders;+ -- Nothing means human-controlled+ , gAiMember :: !(Maybe (Kind.Id StrategyKind))+ -- ^ AI to use for other actors;+ -- Nothing means human-controlled+ , gdipl :: !Dipl -- ^ diplomatic mode+ , gquit :: !(Maybe Status) -- ^ cause of game end/exit+ , gleader :: !(Maybe ActorId)+ , gcolor :: !Color.Color -- ^ color of actors or their frames+ , ginitial :: !Int -- ^ number of initial actors+ , gentry :: !LevelId -- ^ level where initial actors start+ }+ deriving (Show, Eq)++-- | Diplomacy states. Higher overwrite lower in case of assymetric content.+data Diplomacy =+ Unknown+ | Neutral+ | Alliance+ | War+ deriving (Show, Eq, Ord)++type Dipl = EM.EnumMap FactionId Diplomacy++-- | Outcome of a game.+data Outcome =+ Killed -- ^ the faction was eliminated+ | Defeated -- ^ the faction lost the game in another way+ | Camping -- ^ game is supended+ | Conquer -- ^ the player won by eliminating all rivals+ | Escape -- ^ the player escaped the dungeon alive+ | Restart -- ^ game is restarted+ deriving (Show, Eq, Ord)++-- | Current game status.+data Status = Status+ { stOutcome :: Outcome -- ^ current game outcome+ , stDepth :: Int -- ^ depth of the final encounter+ , stInfo :: Text -- ^ extra information+ }+ deriving (Show, Eq, Ord)++-- | Tell whether the faction is controlled (at least partially) by a human.+isHumanFact :: Faction -> Bool+isHumanFact fact = isNothing (gAiLeader fact) || isNothing (gAiMember fact)++-- | Tell whether the faction uses AI to control any of its actors.+usesAIFact :: Faction -> Bool+usesAIFact fact = isJust (gAiLeader fact) || isJust (gAiMember fact)++-- | Tell whether the faction can spawn actors.+isSpawnFact :: Kind.COps -> Faction -> Bool+isSpawnFact Kind.COps{cofact=Kind.Ops{okind}} fact =+ let kind = okind (gkind fact)+ in maybe False (> 0) $ lookup "spawn" $ ffreq kind++-- | Tell whether actors of the faction can be summoned by items, etc..+isSummonFact :: Kind.COps -> Faction -> Bool+isSummonFact Kind.COps{cofact=Kind.Ops{okind}} fact =+ let kind = okind (gkind fact)+ in maybe False (> 0) $ lookup "summon" $ ffreq kind++-- | Check if factions are at war. Assumes symmetry.+isAtWar :: Faction -> FactionId -> Bool+isAtWar fact fid = War == EM.findWithDefault Unknown fid (gdipl fact)++-- | Check if factions are allied. Assumes symmetry.+isAllied :: Faction -> FactionId -> Bool+isAllied fact fid = Alliance == EM.findWithDefault Unknown fid (gdipl fact)++instance Binary Faction where+ put Faction{..} = do+ put gkind+ put gname+ put gconfig+ put gAiLeader+ put gAiMember+ put gdipl+ put gquit+ put gleader+ put gcolor+ put ginitial+ put gentry+ get = do+ gkind <- get+ gname <- get+ gconfig <- get+ gAiLeader <- get+ gAiMember <- get+ gdipl <- get+ gquit <- get+ gleader <- get+ gcolor <- get+ ginitial <- get+ gentry <- get+ return Faction{..}++instance Binary Diplomacy where+ put Unknown = putWord8 0+ put Neutral = putWord8 1+ put Alliance = putWord8 2+ put War = putWord8 3+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Unknown+ 1 -> return Neutral+ 2 -> return Alliance+ 3 -> return War+ _ -> fail "no parse (Diplomacy)"++instance Binary Outcome where+ put Killed = putWord8 0+ put Defeated = putWord8 1+ put Camping = putWord8 2+ put Conquer = putWord8 3+ put Escape = putWord8 4+ put Restart = putWord8 5+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Killed+ 1 -> return Defeated+ 2 -> return Camping+ 3 -> return Conquer+ 4 -> return Escape+ 5 -> return Restart+ _ -> fail "no parse (Outcome)"++instance Binary Status where+ put Status{..} = do+ put stOutcome+ put stDepth+ put stInfo+ get = do+ stOutcome <- get+ stDepth <- get+ stInfo <- get+ return Status{..}
+ Game/LambdaHack/Common/Feature.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE DeriveGeneric #-}+-- | Terrain tile features.+module Game.LambdaHack.Common.Feature+ ( Feature(..)+ ) where++import Data.Binary+import Data.Text (Text)+import GHC.Generics (Generic)++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.+data Feature =+ Ascendable -- ^ triggered by ascending+ | Descendable -- ^ triggered by descending+ | Escapable -- ^ triggered by escaping+ | Openable -- ^ triggered by opening+ | Closable -- ^ triggered by closing++ | Cause !(Effect Int) -- ^ causes the effect when triggered+ | ChangeTo !Text -- ^ transitions to a tile of the group when triggered++ | Walkable -- ^ actors can walk through+ | Clear -- ^ actors can see through+ | Lit -- ^ is 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+ | HiddenAs !Text -- ^ when hidden, looks as a tile of the group+ deriving (Show, Read, Eq, Ord, Generic)++instance Binary Feature
+ Game/LambdaHack/Common/Flavour.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}+-- | The appearance of in-game items, as communicated to the player.+module Game.LambdaHack.Common.Flavour+ ( -- * The @Flavour@ type+ Flavour+ , -- * Constructors+ zipPlain, zipFancy, stdFlav+ , -- * Accessors+ flavourToColor, flavourToName+ -- * Assorted+ , colorToTeamName, colorToPlainName, colorToFancyName+ ) where++import Data.Binary+import qualified Data.Hashable as Hashable+import qualified Data.List as L+import Data.Text (Text)+import GHC.Generics (Generic)++import Game.LambdaHack.Common.Color++-- TODO: add more variety, as the number of items increases+-- | The type of item flavours.+data Flavour = Flavour+ { fancyName :: Bool -- ^ should the colour description be fancy or plain+ , baseColor :: Color -- ^ the colour of the flavour+ }+ deriving (Show, Eq, Ord, Generic)++instance Hashable.Hashable Flavour++instance Binary Flavour++-- | Turn a colour set into a flavour set.+zipPlain, zipFancy :: [Color] -> [Flavour]+zipPlain = L.map (Flavour False)+zipFancy = L.map (Flavour True)++-- | The standard full set of flavours.+stdFlav :: [Flavour]+stdFlav = zipPlain stdCol ++ zipFancy stdCol++-- | Get the underlying base colour of a flavour.+flavourToColor :: Flavour -> Color+flavourToColor Flavour{baseColor} = baseColor++-- | Construct the full name of a flavour.+flavourToName :: Flavour -> Text+flavourToName Flavour{..} | fancyName = colorToFancyName baseColor+flavourToName Flavour{..} = colorToPlainName baseColor++-- | Human-readable names, for item colors. The simple set.+colorToPlainName :: Color -> Text+colorToPlainName Black = "black"+colorToPlainName Red = "red"+colorToPlainName Green = "green"+colorToPlainName Brown = "brown"+colorToPlainName Blue = "blue"+colorToPlainName Magenta = "purple"+colorToPlainName Cyan = "cyan"+colorToPlainName White = "ivory"+colorToPlainName BrBlack = "gray"+colorToPlainName BrRed = "coral"+colorToPlainName BrGreen = "lime"+colorToPlainName BrYellow = "yellow"+colorToPlainName BrBlue = "azure"+colorToPlainName BrMagenta = "pink"+colorToPlainName BrCyan = "aquamarine"+colorToPlainName BrWhite = "white"++-- | Human-readable names, for item colors. The fancy set.+colorToFancyName :: Color -> Text+colorToFancyName Black = "smoky-black"+colorToFancyName Red = "apple-red"+colorToFancyName Green = "forest-green"+colorToFancyName Brown = "mahogany"+colorToFancyName Blue = "royal-blue"+colorToFancyName Magenta = "indigo"+colorToFancyName Cyan = "teal"+colorToFancyName White = "silver-gray"+colorToFancyName BrBlack = "charcoal"+colorToFancyName BrRed = "salmon"+colorToFancyName BrGreen = "emerald"+colorToFancyName BrYellow = "amber"+colorToFancyName BrBlue = "sky-blue"+colorToFancyName BrMagenta = "magenta"+colorToFancyName BrCyan = "turquoise"+colorToFancyName BrWhite = "ghost-white"++-- | Simple names for team colors (bright colours preferred).+colorToTeamName :: Color -> Text+colorToTeamName BrRed = "red"+colorToTeamName BrGreen = "green"+colorToTeamName BrYellow = "yellow"+colorToTeamName BrBlue = "blue"+colorToTeamName BrMagenta = "pink"+colorToTeamName BrCyan = "cyan"+colorToTeamName BrWhite = "white"+colorToTeamName c = colorToFancyName c
+ Game/LambdaHack/Common/HighScore.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}+-- | High score table operations.+module Game.LambdaHack.Common.HighScore+ ( ScoreTable, empty, register, slideshow+ ) 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+import System.Time+import Text.Printf++import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Time++-- | 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+ }+ deriving (Eq, Ord)++-- | 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 stDepth ++ ","+ Defeated -> "was defeated"+ Camping -> "is camping somewhere,"+ Conquer -> "eliminated all opposition"+ Escape -> "emerged victorious"+ Restart -> "resigned prematurely"+ curDate = calendarTimeToString . toUTCTime . date $ score+ big, lil :: String+ big = " "+ lil = " "+ turns = - (negTime score `timeFit` timeTurn)+ -- TODO: the spaces at the end are hand-crafted. Remove when display+ -- of overlays adds such spaces automatically.+ in map T.pack+ [ big+ , printf "%4d. %6d This adventuring party %s after %d turns "+ pos (points score) died turns+ , lil ++ printf "on %s. " curDate+ ]++-- | The list of scores, in decreasing order.+newtype ScoreTable = ScoreTable [ScoreRecord]+ deriving (Eq, Binary)++instance Show ScoreTable where+ show _ = "a score table"++-- | Empty score table+empty :: ScoreTable+empty = ScoreTable []++-- | Insert a new score into the table, Return new table and the ranking.+-- 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+ in (ScoreTable $ prefix ++ [s] ++ take (100 - pos) suffix, pos)++-- | Register a new score in a score table.+register :: ScoreTable -- ^ old table+ -> Int -- ^ the total score. not halved yet+ -> Time -- ^ game time spent+ -> Status -- ^ reason of the game interruption+ -> ClockTime -- ^ current date+ -> 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+ 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 =+ 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 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]++-- | 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 =+ let (_, nlines) = normalLevelBound -- TODO: query terminal size instead+ height = nlines `div` 3+ (subject, person, msgUnless) =+ case stOutcome status of+ Killed | stDepth status <= 1 ->+ ("your short-lived struggle", MU.Sg3rd, "(score halved)")+ Killed ->+ ("your heroic deeds", MU.PlEtc, "(score halved)")+ Defeated ->+ ("your futile efforts", MU.PlEtc, "(score halved)")+ Camping ->+ ("your valiant exploits", MU.PlEtc, "(unless you are slain)")+ Conquer ->+ ("your ruthless victory", MU.Sg3rd,+ if pos <= height+ then "among the greatest heroes"+ else "(score based on time)")+ Escape ->+ ("your dashing coup", MU.Sg3rd,+ if pos <= height+ then "among the greatest heroes"+ else "")+ Restart ->+ ("your abortive attempt", MU.Sg3rd, "(score halved)")+ msg = makeSentence+ [ MU.SubjectVerb person MU.Yes subject "award you"+ , MU.Ordinal pos, "place"+ , msgUnless ]+ in toSlideshow $ map ([msg] ++) $ showCloseScores pos table height++instance Binary ScoreRecord where+ put (ScoreRecord p n (TOD cs cp) s) = do+ put p+ put n+ put cs+ put cp+ put s+ get = do+ p <- get+ n <- get+ cs <- get+ cp <- get+ s <- get+ return (ScoreRecord p n (TOD cs cp) s)
+ Game/LambdaHack/Common/Item.hs view
@@ -0,0 +1,209 @@+{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, OverloadedStrings #-}+-- | Weapons, treasure and all the other items in the game.+-- No operation in this module+-- involves the 'State' or 'Action' type.+-- TODO: Document after it's rethought and rewritten wrt separating+-- inventory manangement and items proper.+module Game.LambdaHack.Common.Item+ ( -- * Teh @Item@ type+ ItemId, Item(..), jkind, buildItem, newItem, viewItem+ -- * Inventory search+ , strongestSearch, strongestSword, strongestRegen+ -- * The item discovery types+ , ItemKindIx, Discovery, DiscoRev, serverDiscos+ -- * The @FlavourMap@ type+ , FlavourMap, emptyFlavourMap, dungeonFlavourMap+ -- * Textual description+ , partItem, partItemWs, partItemAW+ ) where++import Control.Monad+import Data.Binary+import qualified Data.EnumMap.Strict as EM+import qualified Data.Hashable as Hashable+import qualified Data.Ix as Ix+import Data.List+import qualified Data.Set as S+import Data.Text (Text)+import qualified Data.Text as T+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.Kind as Kind+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Utils.Assert++-- | A unique identifier of an item in the dungeon.+newtype ItemId = ItemId Int+ deriving (Show, Eq, Ord, Enum, Binary)++-- | An index of the kind id of an item. Clients have partial knowledge+-- how these idexes map to kind ids. They gain knowledge by identifying items.+newtype ItemKindIx = ItemKindIx Int+ deriving (Show, Eq, Ord, Enum, Ix.Ix, Hashable.Hashable, Binary)++-- | The map of item kind indexes to item kind ids.+-- The full map, as known by the server, is a bijection.+type Discovery = EM.EnumMap ItemKindIx (Kind.Id ItemKind)++-- | The reverse map to @Discovery@, needed for item creation.+type DiscoRev = EM.EnumMap (Kind.Id ItemKind) ItemKindIx++-- TODO: somehow hide from clients jeffect of unidentified items.+-- | Game items in inventories or strewn around the dungeon.+-- The fields @jsymbol@, @jname@ and @jflavour@ make it possible to refer to+-- and draw an unidentified item. Full information about item is available+-- through the @jkindIx@ index as soon as the item is identified.+data Item = Item+ { jkindIx :: !ItemKindIx -- ^ index pointing to the kind of the item+ , jsymbol :: !Char -- ^ individual map symbol+ , jname :: !Text -- ^ individual generic name+ , jflavour :: !Flavour -- ^ individual flavour+ , jeffect :: !(Effect Int) -- ^ the effect when activated+ }+ deriving (Show, Eq, Ord, Generic)++instance Hashable.Hashable Item++instance Binary Item++-- | Recover a kind id of an item, if identified.+jkind :: Discovery -> Item -> Maybe (Kind.Id ItemKind)+jkind disco i = EM.lookup (jkindIx i) disco++serverDiscos :: Kind.Ops ItemKind -> Rnd (Discovery, DiscoRev)+serverDiscos Kind.Ops{obounds, ofoldrWithKey} = do+ let ixs = map ItemKindIx $ take (Ix.rangeSize obounds) [0..]+ shuffle :: Eq a => [a] -> Rnd [a]+ shuffle [] = return []+ shuffle l = do+ x <- oneOf l+ fmap (x :) $ shuffle (delete x l)+ shuffled <- shuffle ixs+ let f ik _ (ikMap, ikRev, ix : rest) =+ (EM.insert ix ik ikMap, EM.insert ik ix ikRev, rest)+ f ik _ (ikMap, _, []) =+ assert `failure` (ik, ikMap, "too short ixs" :: Text)+ (discoS, discoRev, _) =+ ofoldrWithKey f (EM.empty, EM.empty, shuffled)+ return (discoS, discoRev)++-- | Build an item with the given stats.+buildItem :: FlavourMap -> DiscoRev+ -> Kind.Id ItemKind -> ItemKind -> Effect Int -> Item+buildItem (FlavourMap flavour) discoRev ikChosen kind jeffect =+ let jkindIx = discoRev EM.! ikChosen+ jsymbol = isymbol kind+ jname = iname kind+ jflavour =+ case iflavour kind of+ [fl] -> fl+ _ -> flavour EM.! ikChosen+ in Item{..}++-- | Generate an item based on level.+newItem :: Kind.Ops ItemKind -> FlavourMap -> DiscoRev -> Int -> Int+ -> Rnd (Item, Int, ItemKind)+newItem cops@Kind.Ops{opick, okind} flavour discoRev lvl depth = do+ ikChosen <- opick (T.pack "dng") (const True)+ let kind = okind ikChosen+ jcount <- rollDeep lvl depth (icount kind)+ if jcount == 0+ then -- Rare item; beware of inifite loops.+ newItem cops flavour discoRev lvl depth+ else do+ effect <- effectTrav (ieffect kind) (rollDeep lvl depth)+ return ( buildItem flavour discoRev ikChosen kind effect+ , jcount+ , kind )++-- | 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)++emptyFlavourMap :: FlavourMap+emptyFlavourMap = FlavourMap EM.empty++-- | Assigns flavours to item kinds. Assures no flavor is repeated,+-- except for items with only one permitted flavour.+rollFlavourMap :: Kind.Id ItemKind -> ItemKind+ -> Rnd (EM.EnumMap (Kind.Id ItemKind) Flavour, S.Set Flavour)+ -> Rnd (EM.EnumMap (Kind.Id ItemKind) Flavour, S.Set Flavour)+rollFlavourMap key ik rnd =+ let flavours = iflavour ik+ in if length flavours == 1+ then rnd+ else do+ (assocs, available) <- rnd+ let proper = S.fromList flavours `S.intersection` available+ flavour <- oneOf (S.toList proper)+ return (EM.insert key flavour assocs, S.delete flavour available)++-- | Randomly chooses flavour for all item kinds for this game.+dungeonFlavourMap :: Kind.Ops ItemKind -> Rnd FlavourMap+dungeonFlavourMap Kind.Ops{ofoldrWithKey} =+ liftM (FlavourMap . fst) $+ ofoldrWithKey rollFlavourMap (return (EM.empty, S.fromList stdFlav))++strongestItem :: [(ItemId, Item)] -> (Item -> Maybe Int)+ -> Maybe (Int, (ItemId, Item))+strongestItem is p =+ let ks = map (p . snd) is+ in case zip ks is of+ [] -> Nothing+ kis -> case maximum kis of+ (Nothing, _) -> Nothing+ (Just k, iki) -> Just (k, iki)++strongestSearch :: [(ItemId, Item)] -> Maybe (Int, (ItemId, Item))+strongestSearch is =+ strongestItem is $ \ i ->+ case jeffect i of Searching k -> Just k; _ -> Nothing++strongestSword :: Kind.COps -> [(ItemId, Item)] -> Maybe (Int, (ItemId, Item))+strongestSword Kind.COps{corule} is =+ strongestItem is $ \ i ->+ case jeffect i of+ Hurt d k | jsymbol i `elem` ritemMelee (Kind.stdRuleset corule)+ -> Just $ floor (meanDice d) + k+ _ -> Nothing++strongestRegen :: [(ItemId, Item)] -> Maybe (Int, (ItemId, Item))+strongestRegen is =+ strongestItem is $ \ i ->+ case jeffect i of Regeneration k -> Just k; _ -> Nothing++-- | The part of speech describing the item.+partItem :: Kind.Ops ItemKind -> Discovery -> Item -> (MU.Part, MU.Part)+partItem _cops disco i =+ let genericName = jname i+ flav = flavourToName $ jflavour i+ in case jkind disco i of+ Nothing ->+ -- TODO: really hide jeffect from a client that has not discovered+ -- that individual item's properties (nor item kind, if there's only+ -- one effect possible for the kind (plus effect deduction))+ (MU.Text $ flav <+> genericName, "")+ Just _ ->+ let eff = effectToSuffix $ jeffect i+ in (MU.Text genericName, MU.Text eff)++partItemWs :: Kind.Ops ItemKind -> Discovery -> Int -> Item -> MU.Part+partItemWs coitem disco jcount i =+ let (name, stats) = partItem coitem disco i+ in MU.Phrase [MU.CarWs jcount name, stats]++partItemAW :: Kind.Ops ItemKind -> Discovery -> Item -> MU.Part+partItemAW coitem disco i =+ let (name, stats) = partItem coitem disco i+ in MU.AW $ MU.Phrase [name, stats]
+ Game/LambdaHack/Common/Key.hs view
@@ -0,0 +1,232 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Frontend-independent keyboard input operations.+module Game.LambdaHack.Common.Key+ ( Key(..), handleDir, dirAllMoveKey+ , moveBinding, keyTranslate, Modifier(..), KM(..), showKM+ ) where++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 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/++-- | Frontend-independent datatype to represent keys.+data Key =+ Esc+ | Return+ | Space+ | Tab+ | BackTab+ | PgUp+ | PgDn+ | Left+ | Right+ | Up+ | Down+ | End+ | Begin+ | Home+ | KP !Char -- ^ a keypad key for a character (digits and operators)+ | Char !Char -- ^ a single printable character+ | Unknown !String -- ^ an unknown key, registered to warn the user+ deriving (Ord, Eq)++instance Binary Key where+ put Esc = putWord8 0+ put Return = putWord8 1+ put Space = putWord8 2+ put Tab = putWord8 3+ put BackTab = putWord8 4+ put PgUp = putWord8 5+ put PgDn = putWord8 6+ put Left = putWord8 7+ put Right = putWord8 8+ put Up = putWord8 9+ put Down = putWord8 10+ put End = putWord8 11+ put Begin = putWord8 12+ put Home = putWord8 13+ put (KP c) = putWord8 14 >> put c+ put (Char c) = putWord8 15 >> put c+ put (Unknown s) = putWord8 16 >> put s+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Esc+ 1 -> return Return+ 2 -> return Space+ 3 -> return Tab+ 4 -> return BackTab+ 5 -> return PgUp+ 6 -> return PgDn+ 7 -> return Left+ 8 -> return Right+ 9 -> return Up+ 10 -> return Down+ 11 -> return End+ 12 -> return Begin+ 13 -> return Home+ 14 -> fmap KP get+ 15 -> fmap Char get+ 16 -> fmap Unknown get+ _ -> fail "no parse (Key)"++-- | Our own encoding of modifiers. Incomplete.+data Modifier =+ NoModifier+ | Control+ deriving (Ord, Eq)++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)"++data KM = KM {modifier :: !Modifier, key :: !Key}+ deriving (Ord, Eq)++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 {..}++-- Common and terse names for keys.+showKey :: Key -> Text+showKey (Char c) = T.singleton c+showKey Esc = "ESC"+showKey Return = "RET"+showKey Space = "SPACE"+showKey Tab = "TAB"+showKey BackTab = "SHIFT-TAB"+showKey PgUp = "PGUP"+showKey PgDn = "PGDOWN"+showKey Left = "LEFT"+showKey Right = "RIGHT"+showKey Up = "UP"+showKey Down = "DOWN"+showKey End = "END"+showKey Begin = "BEGIN"+showKey Home = "HOME"+showKey (KP c) = "KEYPAD(" <> T.singleton c <> ")"+showKey (Unknown s) = T.pack s++-- | Show a key with a modifier, if any.+showKM :: KM -> Text+showKM KM{modifier=Control, key} = "CTRL-" <> showKey key+showKM KM{modifier=NoModifier, key} = showKey key++dirViChar :: [Char]+dirViChar = ['y', 'k', 'u', 'l', 'n', 'j', 'b', 'h']++dirViMoveKey :: [Key]+dirViMoveKey = map Char dirViChar++dirMoveKey :: [Key]+dirMoveKey = [Home, Up, PgUp, Right, PgDn, Down, End, Left]++dirAllMoveKey :: [Key]+dirAllMoveKey = dirViMoveKey ++ dirMoveKey++dirViRunKey :: [Key]+dirViRunKey = map (Char . Char.toUpper) dirViChar++dirRunKey :: [Key]+dirRunKey = map KP dirNums++_dirAllRunKey :: [Key]+_dirAllRunKey = dirViRunKey ++ dirRunKey++dirNums :: [Char]+dirNums = ['7', '8', '9', '6', '3', '2', '1', '4']++dirHeroKey :: [Key]+dirHeroKey = map Char dirNums++-- | 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++-- TODO: deduplicate+-- | Binding of both sets of movement keys.+moveBinding :: (VectorXY -> a) -> (VectorXY -> 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)++-- | 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.+keyTranslate :: String -> Key+keyTranslate "less" = Char '<'+keyTranslate "greater" = Char '>'+keyTranslate "period" = Char '.'+keyTranslate "colon" = Char ':'+keyTranslate "comma" = Char ','+keyTranslate "question" = Char '?'+keyTranslate "dollar" = Char '$'+keyTranslate "asterisk" = Char '*'+keyTranslate "KP_Multiply" = Char '*'+keyTranslate "slash" = Char '/'+keyTranslate "KP_Divide" = Char '/'+keyTranslate "underscore" = Char '_'+keyTranslate "minus" = Char '-'+keyTranslate "KP_Subtract" = Char '-'+keyTranslate "plus" = Char '+'+keyTranslate "KP_Add" = Char '+'+keyTranslate "bracketleft" = Char '['+keyTranslate "bracketright" = Char ']'+keyTranslate "braceleft" = Char '{'+keyTranslate "braceright" = Char '}'+keyTranslate "Escape" = Esc+keyTranslate "Return" = Return+keyTranslate "space" = Space+keyTranslate "Tab" = Tab+keyTranslate "ISO_Left_Tab" = BackTab+keyTranslate "KP_Up" = Up+keyTranslate "KP_Down" = Down+keyTranslate "KP_Left" = Left+keyTranslate "KP_Right" = Right+keyTranslate "KP_Home" = Home+keyTranslate "KP_End" = End+keyTranslate "KP_Page_Up" = PgUp+keyTranslate "KP_Page_Down" = PgDn+keyTranslate "KP_Begin" = Begin+keyTranslate "KP_Enter" = Return+keyTranslate ['K','P','_',c] = KP c+keyTranslate [c] = Char c+keyTranslate s = Unknown s
+ Game/LambdaHack/Common/Kind.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, 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+ ) where++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 qualified Data.Map.Strict as M+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T++import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.CaveKind+import Game.LambdaHack.Content.FactionKind+import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Content.PlaceKind+import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Content.StrategyKind+import Game.LambdaHack.Content.TileKind+import Game.LambdaHack.Utils.Assert+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++-- | 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+ }++-- | Content operations for the content of type @a@.+data Ops a = Ops+ { osymbol :: Id a -> Char -- ^ the symbol of a content element at id+ , oname :: Id a -> Text -- ^ the name of a content element at id+ , okind :: Id a -> a -- ^ the content element at given id+ , ouniqGroup :: Text -> Id a -- ^ the id of the unique member of+ -- a singleton content group+ , opick :: Text -> (a -> Bool) -> Rnd (Id a)+ -- ^ pick a random id belonging to a group+ -- and satisfying a predicate+ , ofoldrWithKey :: forall b. (Id a -> a -> b -> b) -> b -> b+ -- ^ fold over all content elements of @a@+ , obounds :: (Id a, Id a) -- ^ bounds of identifiers of content @a@+ , ospeedup :: Speedup a -- ^ auxiliary speedup components+ }++-- | Create content operations for type @a@ from definition of content+-- of type @a@.+createOps :: forall a. Show a => ContentDef a -> Ops a+createOps ContentDef{getSymbol, getName, getFreq, content, validate} =+ assert (Id (fromIntegral $ length content) < sentinelId) $+ let kindAssocs :: [(Word8, a)]+ kindAssocs = L.zip [0..] content+ kindMap :: EM.EnumMap (Id a) a+ kindMap = EM.fromDistinctAscList $ L.zip [Id 0..] content+ kindFreq :: M.Map Text (Frequency (Id a, a))+ kindFreq =+ let tuples = [ (group, (n, (Id i, k)))+ | (i, k) <- kindAssocs+ , (group, n) <- getFreq k, n > 0 ]+ f m (group, nik) = M.insertWith (++) group [nik] m+ lists = L.foldl' f M.empty tuples+ nameFreq group = toFreq $ "opick ('" <> group <> "')"+ in M.mapWithKey nameFreq lists+ okind i = fromMaybe (assert `failure` (i, kindMap))+ $ EM.lookup i kindMap+ correct a = not (T.null (getName a)) && L.all ((> 0) . snd) (getFreq a)+ offenders = validate content+ in assert (allB correct content) $+ assert (L.null offenders `blame` ("content failed validation: " :: Text,+ offenders))+ Ops+ { osymbol = getSymbol . okind+ , oname = getName . okind+ , okind = okind+ , ouniqGroup = \ group ->+ let freq = fromMaybe (assert `failure` (group, kindFreq))+ $ M.lookup group kindFreq+ in case runFrequency freq of+ [(n, (i, _))] | n > 0 -> i+ l -> assert `failure` l+ , opick = \ group p ->+ let freq = fromMaybe (assert `failure` (group, kindFreq))+ $ M.lookup group kindFreq+ in frequency $ do+ (i, k) <- freq+ breturn (p k) i+ {- with MonadComprehensions:+ frequency [ i | (i, k) <- kindFreq M.! group, p k ]+ -}+ , ofoldrWithKey = \ f z -> L.foldr (\ (i, a) -> f (Id i) a) z kindAssocs+ , obounds = ( fst $ EM.findMin kindMap+ , fst $ EM.findMax kindMap )+ , ospeedup = undefined -- define elsewhere+ }++-- | Operations for all content types, gathered together.+data COps = COps+ { coactor :: !(Ops ActorKind)+ , cocave :: !(Ops CaveKind)+ , cofact :: !(Ops FactionKind)+ , coitem :: !(Ops ItemKind)+ , coplace :: !(Ops PlaceKind)+ , corule :: !(Ops RuleKind)+ , costrat :: !(Ops StrategyKind)+ , cotile :: !(Ops TileKind)+ }++-- | The standard ruleset used for level operations.+stdRuleset :: Ops RuleKind -> RuleKind+stdRuleset Ops{ouniqGroup, okind} = okind $ ouniqGroup "standard"++instance Show COps where+ show _ = "game content"++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
@@ -0,0 +1,228 @@+{-# LANGUAGE DeriveGeneric #-}+-- | Inhabited dungeon levels and the operations to query and change them+-- as the game progresses.+module Game.LambdaHack.Common.Level+ ( -- * Dungeon+ LevelId, Dungeon, ascendInBranch+ -- * Item containers+ , Container(..)+ -- * The @Level@ type and its components+ , SmellMap, ItemFloor, TileMap+ , Level(..)+ -- * Level update+ , updatePrio, updateSmell, updateFloor, updateTile+ -- * Level query+ , at, atI, accessible, accessibleDir, hideTile+ , findPos, findPosTry, mapLevelActors_, mapDungeonActors_+ ) where++import Data.Binary+import qualified Data.Bits as Bits+import qualified Data.EnumMap.Strict as EM+import qualified Data.List as L+import Data.Text (Text)+import GHC.Generics (Generic)++import Game.LambdaHack.Common.Actor+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Common.Tile+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Content.TileKind+import Game.LambdaHack.Utils.Assert++-- | 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 =+ -- Currently there is just one branch, so the computation is simple.+ let (minD, maxD) =+ case (EM.minViewWithKey dungeon, EM.maxViewWithKey dungeon) of+ (Just ((s, _), _), Just ((e, _), _)) -> (s, e)+ _ -> assert `failure` dungeon+ ln = max minD $ min maxD $ toEnum $ fromEnum lid - k+ in case EM.lookup ln dungeon of+ Just _ | ln /= lid -> [ln]+ _ -> []++-- | Item container type.+data Container =+ CFloor LevelId Point+ | CActor ActorId InvChar+ deriving (Show, Eq, Ord, Generic)++instance Binary Container++-- | Actor time priority queue.+type ActorPrio = EM.EnumMap Time [ActorId]++-- | Items located on map tiles.+type ItemFloor = EM.EnumMap Point ItemBag++-- | Tile kinds on the map.+type TileMap = Kind.Array Point TileKind++-- | Current smell on map tiles.+type SmellMap = EM.EnumMap Point SmellTime++-- | A view on single, inhabited dungeon level. "Remembered" fields+-- carry a subset of the info in the client copies of levels.+data Level = Level+ { ldepth :: !Int -- ^ depth of the level+ , lprio :: !ActorPrio -- ^ remembered actor times on the level+ , lfloor :: !ItemFloor -- ^ remembered items lying on the floor+ , ltile :: !TileMap -- ^ remembered level map+ , lxsize :: !X -- ^ width of the level+ , lysize :: !Y -- ^ height of the level+ , lsmell :: !SmellMap -- ^ remembered smells on the level+ , ldesc :: !Text -- ^ level description+ , lstair :: !(Point, Point) -- ^ destination of (up, down) stairs+ , lseen :: !Int -- ^ currently remembered clear tiles+ , lclear :: !Int -- ^ total number of initially clear tiles+ , ltime :: !Time -- ^ date of the last activity on the level+ , litemNum :: !Int -- ^ number of initial items, 0 for clients+ , lsecret :: !Int -- ^ secret level seed, unknown by clients+ , lhidden :: !Int -- ^ secret tile density, unknown by clients+ }+ deriving (Show, Eq)++-- | Update the actor time priority queue.+updatePrio :: (ActorPrio -> ActorPrio) -> Level -> Level+updatePrio f lvl = lvl {lprio = f (lprio lvl)}++-- | Update the smell map.+updateSmell :: (SmellMap -> SmellMap) -> Level -> Level+updateSmell f lvl = lvl {lsmell = f (lsmell lvl)}++-- | Update the items on the ground map.+updateFloor :: (ItemFloor -> ItemFloor) -> Level -> Level+updateFloor f lvl = lvl {lfloor = f (lfloor lvl)}++-- | Update the tile map.+updateTile :: (TileMap -> TileMap) -> Level -> Level+updateTile f lvl = lvl {ltile = f (ltile lvl)}++assertSparseItems :: ItemFloor -> ItemFloor+assertSparseItems m =+ assert (EM.null (EM.filter EM.null m) `blame` m) m++instance Binary Level where+ put Level{..} = do+ put ldepth+ put lprio+ put (assertSparseItems lfloor)+ put ltile+ put lxsize+ put lysize+ put lsmell+ put ldesc+ put lstair+ put lseen+ put lclear+ put ltime+ put litemNum+ put lsecret+ put lhidden+ get = do+ ldepth <- get+ lprio <- get+ lfloor <- get+ ltile <- get+ lxsize <- get+ lysize <- get+ lsmell <- get+ ldesc <- get+ lstair <- get+ lseen <- get+ lclear <- get+ ltime <- get+ litemNum <- get+ lsecret <- get+ lhidden <- get+ return Level{..}++-- | Query for tile kinds on the map.+at :: Level -> Point -> Kind.Id TileKind+at Level{ltile} p = ltile Kind.! p++-- | Query for items on the ground.+atI :: Level -> Point -> ItemBag+atI Level{lfloor} p = EM.findWithDefault EM.empty p lfloor++-- | 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++-- | Check whether actors can move from a position along a unit vector,+-- using the formula from the standard ruleset.+accessibleDir :: Kind.COps -> Level -> Point -> Vector -> Bool+accessibleDir cops lvl spos dir = accessible cops lvl spos $ spos `shift` dir++hideTile :: Kind.Ops TileKind -> Point -> Level -> Kind.Id TileKind+hideTile cotile p lvl =+ let t = lvl `at` p+ ht = Tile.hiddenAs cotile t -- TODO; tabulate with Speedup?+ in if ht == t+ || (lsecret lvl `Bits.rotateR` fromEnum p `Bits.xor` fromEnum p)+ `mod` lhidden lvl == 0+ then ht+ else t++-- | 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+ if p pos tile+ then return pos+ else search+ in search++-- | Try to find a random position on the map satisfying+-- the conjunction of the list of predicates.+-- If the permitted number of attempts is not enough,+-- try again the same number of times without the first predicate,+-- then without the first two, etc., until only one predicate remains,+-- at which point try as many times, as needed.+findPosTry :: Int -- ^ the number of tries+ -> TileMap -- ^ look up in this map+ -> [Point -> Kind.Id TileKind -> Bool] -- ^ predicates to satisfy+ -> Rnd Point+findPosTry _ ltile [] = findPos ltile (const (const True))+findPosTry _ ltile [p] = findPos ltile p+findPosTry numTries ltile l@(_ : tl) = assert (numTries > 0) $+ let search 0 = findPosTry numTries ltile tl+ search k = do+ pos <- randomR $ Kind.bounds ltile+ let tile = ltile Kind.! pos+ if L.all (\ p -> p pos tile) l+ then return pos+ else search (k - 1)+ in search numTries++mapLevelActors_ :: Monad m => (ActorId -> m a) -> Level -> m ()+mapLevelActors_ f Level{lprio} = do+ let as = concat $ EM.elems lprio+ mapM_ f as++mapDungeonActors_ :: Monad m => (ActorId -> m a) -> Dungeon -> m ()+mapDungeonActors_ f dungeon = do+ let ls = EM.elems dungeon+ mapM_ (mapLevelActors_ f) ls
+ Game/LambdaHack/Common/Misc.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, TypeFamilies #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+-- | Hacks that haven't found their home yet.+module Game.LambdaHack.Common.Misc+ ( normalLevelBound, maxLevelDim, divUp, Freqs, breturn+ , FactionId, LevelId+ ) where++import Control.Monad+import Data.Binary+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.Functor+import Data.Key+import Data.Text (Text)+import Data.Traversable (traverse)++-- | 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)++-- | Integer division, rounding up.+divUp :: Int -> Int -> Int+divUp n k = (n + k - 1) `div` k++-- | For each group that the kind belongs to, denoted by a @Text@ name+-- in the first component of a pair, the second component of a pair shows+-- how common the kind is within the group.+type Freqs = [(Text, Int)]++-- | @breturn b a = [a | b]@+breturn :: MonadPlus m => Bool -> a -> m a+breturn True a = return a+breturn False _ = mzero++-- Data.Binary++instance (Enum k, Binary k, Binary e) => Binary (EM.EnumMap k e) where+ put m = put (EM.size m) >> mapM_ put (EM.toAscList m)+ get = liftM EM.fromDistinctAscList get++instance (Enum k, Binary k) => Binary (ES.EnumSet k) where+ put m = put (ES.size m) >> mapM_ put (ES.toAscList m)+ get = liftM ES.fromDistinctAscList get++-- Data.Key++type instance Key (EM.EnumMap k) = k++instance Zip (EM.EnumMap k) where+ zipWith = EM.intersectionWith++instance Enum k => ZipWithKey (EM.EnumMap k) where+ zipWithKey = EM.intersectionWithKey++instance Enum k => Keyed (EM.EnumMap k) where+ mapWithKey = EM.mapWithKey++instance Enum k => FoldableWithKey (EM.EnumMap k) where+ foldrWithKey = EM.foldrWithKey++instance Enum k => TraversableWithKey (EM.EnumMap k) where+ traverseWithKey f = fmap EM.fromDistinctAscList+ . traverse (\(k, v) -> (,) k <$> f k v) . EM.toAscList++instance Enum k => Indexable (EM.EnumMap k) where+ index = (EM.!)++instance Enum k => Lookup (EM.EnumMap k) where+ lookup = EM.lookup++instance Enum k => Adjustable (EM.EnumMap k) where+ adjust = EM.adjust++-- | A unique identifier of a faction in a game.+newtype FactionId = FactionId Int+ deriving (Show, Eq, Ord, Enum)++instance Binary FactionId where+ put (FactionId n) = put n+ get = fmap FactionId get++-- | Abstract level identifiers.+newtype LevelId = LevelId Int+ deriving (Show, Eq, Ord, Enum, Read)++instance Binary LevelId where+ put (LevelId n) = put n+ get = fmap LevelId get
+ Game/LambdaHack/Common/Msg.hs view
@@ -0,0 +1,219 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}+-- | 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+ , Report, emptyReport, nullReport, singletonReport, addMsg+ , splitReport, renderReport, findInReport+ , History, emptyHistory, singletonHistory, mergeHistory+ , addReport, renderHistory, takeHistory+ , Overlay, stringByLocation+ , Slideshow(runSlideshow), 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.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Game.LambdaHack.Utils.Assert+import NLP.Miniutter.English (showT, (<+>), (<>))+import qualified NLP.Miniutter.English as MU++import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.PointXY++-- | Re-exported English phrase creation functions, applied to default+-- irregular word sets.+makePhrase, makeSentence :: [MU.Part] -> Text+makePhrase = MU.makePhrase MU.defIrregular+makeSentence = MU.makeSentence MU.defIrregular++-- | The type of a single message.+type Msg = Text++-- | The \"press something to see more\" mark.+moreMsg :: Msg+moreMsg = "--more-- "++-- | The confirmation request message.+yesnoMsg :: Msg+yesnoMsg = "[yn]"++-- | Add a space at the message end, for display overlayed over the level map.+-- Also trims (does not wrap!) too long lines. In case of newlines,+-- displays only the first line, but marks the message as partial.+truncateMsg :: X -> Text -> Text+truncateMsg w xsRaw =+ let xs = case T.lines xsRaw of+ [] -> xsRaw+ [line] -> line+ line : _ -> line <> T.replicate (w + 1) " "+ len = T.length xs+ in case compare w len of+ LT -> T.snoc (T.take (w - 1) xs) '$'+ EQ -> xs+ GT -> if T.null xs || T.last xs == ' '+ then xs+ else T.snoc xs ' '++-- | The type of a set of messages to show at the screen at once.+newtype Report = Report [(BS.ByteString, Int)]+ deriving (Show)++instance Binary Report where+ put (Report x) = put x+ get = fmap Report get++-- | Empty set of messages.+emptyReport :: Report+emptyReport = Report []++-- | Test if the set of messages is empty.+nullReport :: Report -> Bool+nullReport (Report l) = null l++-- | Construct a singleton set of messages.+singletonReport :: Msg -> Report+singletonReport = addMsg emptyReport++-- TODO: Differentiate from msgAdd. Generally, invent more informative names.+-- | Add message to the end of report.+addMsg :: Report -> Msg -> Report+addMsg r m | T.null m = r+addMsg (Report ((x, n) : xns)) y' | x == y =+ Report $ (y, n + 1) : xns+ where y = encodeUtf8 y'+addMsg (Report xns) y = Report $ (encodeUtf8 y, 1) : xns++-- | Split a messages into chunks that fit in one line.+-- We assume the width of the messages line is the same as of level map.+splitReport :: Report -> [Text]+splitReport r =+ let w = fst normalLevelBound + 1+ in splitText w $ renderReport r++-- | Render a report as a (possibly very long) string.+renderReport :: Report -> Text+renderReport (Report []) = T.empty+renderReport (Report (xn : xs)) =+ renderReport (Report xs) <+> renderRepetition xn++renderRepetition :: (BS.ByteString, Int) -> Text+renderRepetition (s, 1) = decodeUtf8 s+renderRepetition (s, n) = decodeUtf8 s <> "<x" <> showT n <> ">"++findInReport :: (BS.ByteString -> Bool) -> Report -> Maybe BS.ByteString+findInReport f (Report xns) = find f $ map fst xns++-- | Split a string into lines. Avoids ending the line with a character+-- 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' :: 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+ 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++-- | Empty history of reports.+emptyHistory :: History+emptyHistory = History []++-- | Construct a singleton history of reports.+singletonHistory :: Report -> History+singletonHistory r = addReport r emptyHistory++mergeHistory :: [(Msg, History)] -> History+mergeHistory l =+ let unhist (History x) = x+ f (msg, h) = singletonReport msg : unhist h+ in History $ concatMap f l++-- | Render history as many lines of text, wrapping if necessary.+renderHistory :: History -> Overlay+renderHistory (History h) = concatMap splitReport h++-- | Add a report to history, handling repetitions.+addReport :: Report -> History -> History+addReport (Report []) h = h+addReport m (History []) = History [m]+addReport (Report m) (History (Report h : hs)) =+ case (reverse m, h) of+ ((s1, n1) : rs, (s2, n2) : hhs) | s1 == s2 ->+ let hist = Report ((s2, n1 + n2) : hhs) : hs+ in History $ if null rs then hist else Report (reverse rs) : hist+ _ -> History $ Report m : Report h : hs++-- | Take the given prefix of reports from a history.+takeHistory :: Int -> History -> History+takeHistory k (History h) = History $ take k h++-- | 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]++-- | 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)++-- | 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 = assert (length msg <= lysize) $+ 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++-- | 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)++-- | Declare the list of 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
+ Game/LambdaHack/Common/Perception.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, OverloadedStrings #-}+-- | Actors perceiving other actors and the dungeon level.+module Game.LambdaHack.Common.Perception+ ( Perception(..), PerceptionVisible(..), PerActor+ , totalVisible, smellVisible+ , actorSeesLoc, nullPer, addPer, diffPer, smellFromActors+ , FactionPers, Pers+ ) where++import Data.Binary+import qualified Data.EnumMap.Strict as EM+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}+ deriving (Show, Eq, Binary)++type PerActor = EM.EnumMap ActorId PerceptionVisible++-- | 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+ }+ deriving (Show, Eq, Generic)++instance Binary Perception++-- | Perception of a single faction, indexed by level identifier.+type FactionPers = EM.EnumMap LevelId Perception++-- | Perception indexed by faction identifier.+-- This can't be added to @FactionDict@, because clients can't see it.+type Pers = EM.EnumMap FactionId FactionPers++-- | The set of tiles visible by at least one hero.+totalVisible :: Perception -> ES.EnumSet Point+totalVisible = pvisible . ptotal++-- | The set of tiles smelled by at least one hero.+smellVisible :: Perception -> ES.EnumSet Point+smellVisible = pvisible . psmell++-- | Whether an actor can see a position.+actorSeesLoc :: Perception -> ActorId -> Point -> Bool+actorSeesLoc 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)++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+ }++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
+ Game/LambdaHack/Common/Point.hs view
@@ -0,0 +1,113 @@+{-# 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+ ) where++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 Game.LambdaHack.Common.Area+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.VectorXY+import Game.LambdaHack.Utils.Assert++-- | 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)++instance Binary Point where+ put (Point n) = put n+ get = fmap Point get++-- For debugging.+instance Show Point where+ show (Point n) = show n++-- | Print a point as a tuple of cartesian coordinates.+showPoint :: X -> Point -> Text+showPoint lxsize = showT . fromPoint lxsize++-- | Conversion from cartesian coordinates to @Point@.+toPoint :: X -> PointXY -> Point+toPoint lxsize (PointXY (x, y)) =+ assert (lxsize > x && x >= 0 && y >= 0 `blame` (lxsize, x, y))+ $ Point $ x + y * lxsize++-- | Conversion from @Point@ to cartesian coordinates.+fromPoint :: X -> Point -> PointXY+fromPoint lxsize (Point p) =+ assert (p >= 0 `blame` (lxsize, p))+ $ PointXY (p `rem` lxsize, p `quot` lxsize)++-- | The top-left corner position of the level.+origin :: Point+origin = Point 0++-- | 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)++-- | 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++-- | Checks that a point belongs to an area.+inside :: X -> Point -> Area -> 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)++-- | 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.+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
+ Game/LambdaHack/Common/PointXY.hs view
@@ -0,0 +1,73 @@+-- | Basic cartesian geometry operations on 2D points.+module Game.LambdaHack.Common.PointXY+ ( X, Y, PointXY(..), fromTo, sortPointXY, blaXY+ ) where++import qualified Data.List as L++import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Utils.Assert++-- | 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` ((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)
+ Game/LambdaHack/Common/Random.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE DeriveGeneric #-}+-- | Representation of probabilities and random computations.+module Game.LambdaHack.Common.Random+ ( -- * The @Rng@ monad+ Rnd+ -- * Random operations+ , randomR, random, oneOf, frequency, roll+ -- * Rolling dice+ , RollDice(..), rollDice, maxDice, minDice, meanDice+ -- * Rolling 2D coordinates+ , RollDiceXY(..), rollDiceXY+ -- * Rolling dependent on depth+ , RollDeep, rollDeep, chanceDeep, intToDeep, maxDeep+ -- * Fractional chance+ , Chance, chance+ -- * Run using the IO RNG+ , rndToIO+ ) where++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 Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.Frequency++-- | The monad of computations with random generator state.+-- The lazy state monad is OK here: the state is small and regularly forced.+type Rnd a = St.State R.StdGen a++-- | Get a random object within a range with a uniform distribution.+randomR :: (R.Random a) => (a, a) -> Rnd a+randomR range = St.state $ R.randomR range++-- | Get a random object of a given type with a uniform distribution.+random :: (R.Random a) => Rnd a+random = St.state R.random++-- | Get any element of a list with equal probability.+oneOf :: [a] -> Rnd a+oneOf xs = do+ r <- randomR (0, length xs - 1)+ return (xs !! r)++-- | Gen an element according to a frequency distribution.+frequency :: Show a => Frequency a -> Rnd a+frequency fr = St.state $ rollFreq fr++-- | Roll a single die.+roll :: Int -> Rnd Int+roll x = if x <= 0 then return 0 else randomR (1, x)++-- | Dice: 1d7, 3d3, 1d0, etc.+-- @RollDice a b@ represents @a@ rolls of @b@-sided die.+data RollDice = RollDice Binary.Word8 Binary.Word8+ deriving (Eq, Ord, Generic)++instance Show RollDice where+ show (RollDice a b) = show a ++ "d" ++ show b++instance Read RollDice where+ readsPrec d s =+ let (a, db) = L.break (== 'd') s+ av = read a+ in case db of+ 'd' : b -> [ (RollDice av bv, rest) | (bv, rest) <- readsPrec d b ]+ _ -> []++instance Hashable.Hashable RollDice++instance Binary RollDice++-- | Roll dice and sum the results.+rollDice :: RollDice -> Rnd Int+rollDice (RollDice a' 1) = return $ fromEnum a' -- optimization+rollDice (RollDice a' b') =+ let (a, b) = (fromEnum a', fromEnum b')+ in liftM sum (replicateM a (roll b))++-- | Maximal value of dice.+maxDice :: RollDice -> Int+maxDice (RollDice a' b') =+ let (a, b) = (fromEnum a', fromEnum b')+ in a * b++-- | Minimal value of dice.+minDice :: RollDice -> Int+minDice (RollDice a' b') =+ let (a, b) = (fromEnum a', fromEnum b')+ in if b == 0 then 0 else a++-- | Mean value of dice.+meanDice :: RollDice -> Rational+meanDice (RollDice a' b') =+ let (a, b) = (fromIntegral a', fromIntegral b')+ in if b' == 0 then 0 else a * (b + 1) % 2++-- | Dice for rolling a pair of integer parameters pertaining to,+-- respectively, the X and Y cartesian 2D coordinates.+data RollDiceXY = RollDiceXY (RollDice, RollDice)+ deriving Show++-- | Roll the two sets of dice.+rollDiceXY :: RollDiceXY -> Rnd (Int, Int)+rollDiceXY (RollDiceXY (xd, yd)) = do+ x <- rollDice xd+ y <- rollDice yd+ return (x, y)++-- | Dice for parameters scaled with current level depth.+-- 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.+type RollDeep = (RollDice, RollDice)++-- | Roll dice scaled with current level depth.+-- Note that at the first level, the scaled dice are always ignored.+rollDeep :: Int -> Int -> RollDeep -> Rnd Int+rollDeep n depth (d1, d2) =+ assert (n > 0 && n <= depth `blame` (n, depth)) $ do+ r1 <- rollDice d1+ r2 <- rollDice d2+ return $ r1 + ((n - 1) * r2) `div` max 1 (depth - 1)++-- | Roll dice scaled with current level depth and return @True@+-- if the results if greater than 50.+chanceDeep :: Int -> Int -> RollDeep -> Rnd Bool+chanceDeep n depth deep = do+ c <- rollDeep n depth deep+ return $ c > 50++-- | Generate a @RollDeep@ that always gives a constant integer.+intToDeep :: Int -> RollDeep+intToDeep 0 = (RollDice 0 0, RollDice 0 0)+intToDeep n' = let n = toEnum n'+ in if n > maxBound || n < minBound+ then assert `failure` n'+ else (RollDice n 1, RollDice 0 0)++-- | Maximal value of scaled dice.+maxDeep :: RollDeep -> Int+maxDeep (d1, d2) = maxDice d1 + maxDice d2++-- | Fractional chance.+type Chance = Rational++-- | Give @True@, with probability determined by the fraction.+chance :: Chance -> Rnd Bool+chance r = do+ let n = numerator r+ d = denominator r+ k <- randomR (1, d)+ return (k <= n)++rndToIO :: Rnd a -> IO a+rndToIO r = do+ g <- R.getStdGen+ let (x, ng) = St.runState r g+ R.setStdGen ng+ return x
+ Game/LambdaHack/Common/ServerCmd.hs view
@@ -0,0 +1,50 @@+-- | Abstract syntax of server commands.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Common.ServerCmd+ ( CmdSer(..), aidCmdSer+ ) where++import Data.Text (Text)++import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.Vector++-- | Abstract syntax of server commands.+data CmdSer =+ MoveSer ActorId Vector+ | ExploreSer ActorId Vector+ | RunSer ActorId Vector+ | WaitSer ActorId+ | PickupSer ActorId ItemId Int InvChar+ | DropSer ActorId ItemId+ | ProjectSer ActorId Point Int ItemId Container+ | ApplySer ActorId ItemId Container+ | TriggerSer ActorId Point+ | SetPathSer ActorId [Vector]+ | GameRestartSer ActorId Text+ | GameExitSer ActorId+ | GameSaveSer ActorId+ | CfgDumpSer ActorId+ deriving (Show)++-- | The actor performing the command, if still alive afterwards.+aidCmdSer :: CmdSer -> ActorId+aidCmdSer cmd = case cmd of+ MoveSer aid _ -> aid+ ExploreSer aid _ -> aid+ RunSer aid _ -> aid+ WaitSer aid -> aid+ PickupSer aid _ _ _ -> aid+ DropSer aid _ -> aid+ ProjectSer aid _ _ _ _ -> aid+ ApplySer aid _ _ -> aid+ TriggerSer aid _ -> aid+ SetPathSer aid _ -> aid+ GameRestartSer aid _ -> aid+ GameExitSer aid -> aid+ GameSaveSer aid -> aid+ CfgDumpSer aid -> aid
+ Game/LambdaHack/Common/State.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Server and client game state types and operations.+module Game.LambdaHack.Common.State+ ( -- * Basic game state, local or global+ State+ -- * State components+ , sdungeon, sdepth, sactorD, sitemD, sfactionD, stime, scops, shigh+ -- * State operations+ , defStateGlobal, emptyState, localFromGlobal+ , updateDungeon, updateDepth, updateActorD, updateItemD+ , updateFaction, updateTime, updateCOps, getLocalTime+ , isHumanFaction, usesAIFaction, isSpawnFaction, isSummonFaction+ ) where++import Data.Binary+import qualified Data.EnumMap.Strict as EM+import Data.Text (Text)++import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.HighScore as HighScore+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.Time+import Game.LambdaHack.Content.TileKind++-- | View on game state. "Remembered" fields carry a subset of the info+-- in the client copies of the state. Clients never directly change+-- their @State@, but apply atomic actions sent by the server to do so.+data State = State+ { _sdungeon :: !Dungeon -- ^ remembered dungeon+ , _sdepth :: !Int -- ^ dungeon \'depth\' for items creation, etc.+ , _sactorD :: !ActorDict -- ^ remembered actors in the dungeon+ , _sitemD :: !ItemDict -- ^ remembered items in the dungeon+ , _sfactionD :: !FactionDict -- ^ remembered sides still in game+ , _stime :: !Time -- ^ global game time+ , _scops :: Kind.COps -- ^ remembered content+ , _shigh :: !HighScore.ScoreTable -- ^ high score table+ }+ deriving (Show, Eq)++-- 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+ -> Level+unknownLevel Kind.Ops{ouniqGroup} ldepth lxsize lysize ldesc lstair lclear =+ let unknownId = ouniqGroup "unknown space"+ in Level { ldepth+ , lprio = EM.empty+ , lfloor = EM.empty+ , ltile = unknownTileMap unknownId lxsize lysize+ , lxsize = lxsize+ , lysize = lysize+ , lsmell = EM.empty+ , ldesc+ , lstair+ , lseen = 0+ , lclear+ , ltime = timeZero+ , litemNum = 0+ , lsecret = 0 -- unkown by clients+ , lhidden = 0 -- unkown by clients+ }++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)++-- | Initial complete global game state.+defStateGlobal :: Dungeon -> Int+ -> FactionDict -> Kind.COps -> HighScore.ScoreTable+ -> State+defStateGlobal _sdungeon _sdepth _sfactionD _scops _shigh =+ State+ { _sactorD = EM.empty+ , _sitemD = EM.empty+ , _stime = timeZero+ , ..+ }++-- | Initial empty state.+emptyState :: State+emptyState =+ State+ { _sdungeon = EM.empty+ , _sdepth = 0+ , _sactorD = EM.empty+ , _sitemD = EM.empty+ , _sfactionD = EM.empty+ , _stime = timeZero+ , _scops = undefined+ , _shigh = HighScore.empty+ }++-- TODO: make lstair secret until discovered; use this later on for+-- goUp in targeting mode (land on stairs of on the same location up a level+-- if this set of stsirs is unknown).+-- | Local state created by removing secret information from global+-- state components.+localFromGlobal :: State -> State+localFromGlobal State{_scops=_scops@Kind.COps{cotile}, .. } =+ State+ { _sdungeon =+ EM.map (\Level{ldepth, lxsize, lysize, ldesc, lstair, lclear} ->+ unknownLevel cotile ldepth lxsize lysize ldesc lstair lclear)+ _sdungeon+ , ..+ }++-- | Update dungeon data within state.+updateDungeon :: (Dungeon -> Dungeon) -> State -> State+updateDungeon f s = s {_sdungeon = f (_sdungeon s)}++-- | Update dungeon depth.+updateDepth :: (Int -> Int) -> State -> State+updateDepth f s = s {_sdepth = f (_sdepth s)}++-- | Update the actor dictionary.+updateActorD :: (ActorDict -> ActorDict) -> State -> State+updateActorD f s = s {_sactorD = f (_sactorD s)}++-- | Update the item dictionary.+updateItemD :: (ItemDict -> ItemDict) -> State -> State+updateItemD f s = s {_sitemD = f (_sitemD s)}++-- | Update faction data within state.+updateFaction :: (FactionDict -> FactionDict) -> State -> State+updateFaction f s = s {_sfactionD = f (_sfactionD s)}++-- | Update global time within state.+updateTime :: (Time -> Time) -> State -> State+updateTime f s = s {_stime = f (_stime s)}++-- | Update content data within state.+updateCOps :: (Kind.COps -> Kind.COps) -> State -> State+updateCOps f s = s {_scops = f (_scops s)}++-- | Get current time from the dungeon data.+getLocalTime :: LevelId -> State -> Time+getLocalTime lid s = ltime $ _sdungeon s EM.! lid++-- | Tell whether the faction is controlled (at least partially) by a human.+isHumanFaction :: FactionId -> State -> Bool+isHumanFaction fid s = isHumanFact $ _sfactionD s EM.! fid++-- | Tell whether the faction uses AI to control any of its actors.+usesAIFaction :: FactionId -> State -> Bool+usesAIFaction fid s = usesAIFact $ _sfactionD s EM.! fid++-- | 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++sdungeon :: State -> Dungeon+sdungeon = _sdungeon++sdepth :: State -> Int+sdepth = _sdepth++sactorD :: State -> ActorDict+sactorD = _sactorD++sitemD :: State -> ItemDict+sitemD = _sitemD++sfactionD :: State -> FactionDict+sfactionD = _sfactionD++stime :: State -> Time+stime = _stime++scops :: State -> Kind.COps+scops = _scops++shigh :: State -> HighScore.ScoreTable+shigh = _shigh++instance Binary State where+ put State{..} = do+ put _sdungeon+ put _sdepth+ put _sactorD+ put _sitemD+ put _sfactionD+ put _stime+ put _shigh+ get = do+ _sdungeon <- get+ _sdepth <- get+ _sactorD <- get+ _sitemD <- get+ _sfactionD <- get+ _stime <- get+ _shigh <- get+ let _scops = undefined -- overwritten by recreated cops+ return State{..}
+ Game/LambdaHack/Common/Tile.hs view
@@ -0,0 +1,104 @@+-- | Operations concerning dungeon level tiles.+--+-- Unlike for many other content types, there is no type @Tile@,+-- of particular concrete tiles in the dungeon,+-- corresponding to 'TileKind' (the type of kinds of terrain tiles).+-- This is because the tiles are too numerous and there's not enough+-- storage space for a well-rounded @Tile@ type, on one hand,+-- and on the other hand, tiles are accessed+-- too often in performance critical code+-- to try to compress their representation and/or recompute them.+-- Instead, of defining a @Tile@ type, we express various properties+-- of concrete tiles by arrays or sparse EnumMaps, as appropriate.+--+-- 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+ , changeTo, hiddenAs+ ) where++import qualified Data.Array.Unboxed as A+import qualified Data.List as L++import Game.LambdaHack.Content.TileKind+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++-- | The last time a hero left a smell in a given tile. To be used+-- by monsters that hunt by smell.+type SmellTime = Time++-- | Whether a tile kind has the given feature.+kindHasFeature :: F.Feature -> TileKind -> Bool+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)++-- | 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 = Kind.TileSpeedup{isClearTab}} = isClearTab++-- | 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 = Kind.TileSpeedup{isLitTab}} = isLitTab++-- | Whether a tile can be explored, possibly yielding a treasure+-- or a hidden message. We exclude doors and hidden features.+isExplorable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+isExplorable cops tk =+ not (hasFeature cops F.Closable tk)+ && (isClear cops tk+ || hasFeature cops F.Walkable tk)++-- | The player can't tell one tile from the other.+similar :: TileKind -> TileKind -> Bool+similar 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}++changeTo :: Kind.Ops TileKind -> Kind.Id TileKind -> Rnd (Kind.Id TileKind)+changeTo Kind.Ops{okind, opick} t =+ let getTo (F.ChangeTo group) _ = Just group+ getTo _ acc = acc+ in case foldr getTo Nothing (tfeature (okind t)) of+ Nothing -> return t+ Just group -> opick group (const True)++hiddenAs :: Kind.Ops TileKind -> Kind.Id TileKind -> Kind.Id TileKind+hiddenAs Kind.Ops{okind, ouniqGroup} t =+ let getTo (F.HiddenAs group) _ = Just group+ getTo _ acc = acc+ in case foldr getTo Nothing (tfeature (okind t)) of+ Nothing -> t+ Just group -> ouniqGroup group
+ Game/LambdaHack/Common/Time.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+-- | Game time and speed.+module Game.LambdaHack.Common.Time+ ( Time, timeZero, timeClip, timeTurn+ , timeAdd, timeFit, timeNegate, timeScale, timeEpsilon+ , timeToDigit+ , Speed, toSpeed, speedZero, speedNormal, speedScale, speedAdd, speedNegate+ , ticksPerMeter, traveled, speedFromWeight, rangeFromSpeed+ ) where++import Data.Binary+import qualified Data.Char as Char+import Data.Int (Int64)++-- | Game time in ticks. The time dimension.+-- One tick is 1 microsecond (one millionth of a second),+-- one turn is 0.5 s.+newtype Time = Time Int64+ deriving (Show, Eq, Ord, Enum)++instance Binary Time where+ put (Time n) = put n+ get = fmap Time get++-- | Start of the game time, or zero lenght time interval.+timeZero :: Time+timeZero = Time 0++-- | The smallest unit of time. Do not export, because the proportion+-- of turn to tick is an implementation detail.+-- The significance of this detail is only that it determines resolution+-- of the time dimension.+_timeTick :: Time+_timeTick = Time 1++-- | An infinitesimal time period.+timeEpsilon :: Time+timeEpsilon = _timeTick++-- TODO: don't have a fixed time, but instead set it at 1/3 or 1/4+-- of timeTurn depending on level. Clips are a UI feature+-- after all, so should depend on the user situation.+-- | At least once per clip all moves are resolved and a frame+-- or a frame delay is generated.+-- Currently one clip is 0.1 s, but it may change,+-- and the code should not depend on this fixed value.+timeClip :: Time+timeClip = Time 100000++-- | One turn is 0.5 s. The code may depend on that.+-- Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m).+timeTurn :: Time+timeTurn = Time 500000++-- | This many turns fit in a single second.+turnsInSecond :: Int64+turnsInSecond = 2++-- | This many ticks fits in a single second. Do not export,+_ticksInSecond :: Int64+_ticksInSecond =+ let Time ticksInTurn = timeTurn+ in ticksInTurn * turnsInSecond++-- | Time addition.+timeAdd :: Time -> Time -> Time+timeAdd (Time t1) (Time t2) = Time (t1 + t2)++-- | How many time intervals of the latter kind fits in an interval+-- of the former kind.+timeFit :: Time -> Time -> Int+timeFit (Time t1) (Time t2) = fromIntegral $ t1 `div` t2++-- | Negate a time interval. Can be used to subtract from a time+-- or to reverse the ordering on time.+timeNegate :: Time -> Time+timeNegate (Time t) = Time (-t)++-- | Scale time by an @Int@ scalar value.+timeScale :: Time -> Int -> Time+timeScale (Time t) s = Time (t * fromIntegral s)++-- | Represent the main 10 thresholds of a time range by digits,+-- given the total length of the time range.+timeToDigit :: Time -> Time -> Char+timeToDigit (Time maxT) (Time t) =+ let k = 10 * t `div` maxT+ digit | k > 9 = '*'+ | k < 0 = '-'+ | otherwise = Char.intToDigit $ fromIntegral k+ in digit++-- | Speed in meters per 1 million seconds (m/Ms).+-- Actors at normal speed (2 m/s) take one time turn (0.5 s)+-- to move one tile (1 m by 1 m).+newtype Speed = Speed Int64+ deriving (Show, Eq, Ord)++instance Binary Speed where+ put (Speed n) = put n+ get = fmap Speed get++-- | Number of seconds in a kilo-second.+sInMs :: Int64+sInMs = 1000000++-- | Constructor for content definitions.+toSpeed :: Double -> Speed+toSpeed s = Speed $ round $ s * fromIntegral sInMs++-- | No movement possible at that speed.+speedZero :: Speed+speedZero = Speed 0++-- | Normal speed (2 m/s) that suffices to move one tile in one turn.+speedNormal :: Speed+speedNormal = Speed $ 2 * sInMs++-- | Scale speed by an @Int@ scalar value.+speedScale :: Rational -> Speed -> Speed+speedScale s (Speed v) = Speed (round $ fromIntegral v * s)++-- | Speed addition.+speedAdd :: Speed -> Speed -> Speed+speedAdd (Speed s1) (Speed s2) = Speed (s1 + s2)++-- | Speed negation.+speedNegate :: Speed -> Speed+speedNegate (Speed n) = Speed (-n)++-- | 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)++-- | Calculate projectile speed from item weight in grams+-- and speed bonus in percents.+-- See <https://github.com/kosmikus/LambdaHack/wiki/Item-statistics>.+speedFromWeight :: Int -> Int -> Speed+speedFromWeight weight bonus =+ let w = fromIntegral weight+ b = fromIntegral bonus+ 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++-- | 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,+-- that is 2 turns, and then drops to the ground.+-- Dividing and multiplying by 2 ensures both turns of flight+-- cover the same distance.+rangeFromSpeed :: Speed -> Int+rangeFromSpeed (Speed v) = fromIntegral $ 2 * (v `div` (sInMs * 2))
+ Game/LambdaHack/Common/Vector.hs view
@@ -0,0 +1,161 @@+-- | 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+ ) where++import Data.Binary++import Game.LambdaHack.Common.Area+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.VectorXY+import Game.LambdaHack.Utils.Assert++-- | 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)++instance Binary Vector where+ put (Vector dir) = put dir+ get = fmap Vector get++-- For debugging.+instance Show Vector where+ show (Vector n) = show n++-- | Converts a vector in cartesian representation into @Vector@.+toVector :: X -> VectorXY -> Vector+toVector lxsize (VectorXY (x, y)) =+ Vector $ x + y * lxsize++isUnitXY :: VectorXY -> Bool+isUnitXY v = chessDistXY v == 1++-- | Tells if a vector has length 1 in the chessboard metric.+isUnit :: X -> Vector -> Bool+isUnit lxsize = isUnitXY . fromDir lxsize++-- | 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` (lxsize, v)) $+ Vector $ x + y * lxsize++-- | 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` (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++-- | 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++-- | Translate a point by a vector, but only if the result fits in an area.+shiftBounded :: X -> Area -> Point -> Vector -> Point+shiftBounded lxsize area pos dir =+ let res = shift pos dir+ in if inside lxsize res area then res else pos++-- | Vectors of all unit moves, clockwise, starting north-west.+moves :: X -> [Vector]+moves lxsize = map (toDir lxsize) movesXY++-- | 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)++-- | 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++-- | Reverse an arbirary vector.+neg :: Vector -> Vector+neg (Vector dir) = Vector (-dir)++-- 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)) =+ assert (dx /= 0 || dy /= 0 `blame` (dx, dy)) $+ let angle :: Double+ angle = atan (fromIntegral dy / fromIntegral dx) / (pi / 2)+ dxy | angle <= -0.75 = (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` (lxsize, dx, dy, angle)+ rxy = if dx >= 0+ then VectorXY dxy+ else negXY $ VectorXY dxy+ in assert (not (isUnitXY v) || v == rxy `blame` (v, rxy))+ $ toDir lxsize rxy++-- TODO: Perhaps produce all acceptable directions and let AI choose.+-- That would also eliminate the Doubles. Or only directions from bla?+-- Smart monster could really use all dirs to be less predictable,+-- but it wouldn't look as natural as bla, so for less smart bla is better.+-- | Given two distinct positions, determine the direction (a unit vector)+-- in which one should move from the first in order to get closer+-- to the second. Ignores obstacles. Of several equally good directions+-- (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` (pos0, pos1)) $+ let v = displacementXYZ lxsize pos0 pos1+ in normalize lxsize v++-- | 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++-- | A list of vectors between a list of points.+displacePath :: [Point] -> [Vector]+displacePath [] = []+displacePath lp1@(_ : lp2) = zipWith displacement lp1 lp2++-- | 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
+ Game/LambdaHack/Common/VectorXY.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+-- | Basic cartesian geometry operations on 2D vectors.+module Game.LambdaHack.Common.VectorXY+ ( VectorXY(..), shiftXY, movesXY, movesCardinalXY+ , chessDistXY, euclidDistSqXY, negXY+ ) 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)
− Game/LambdaHack/Config.hs
@@ -1,123 +0,0 @@--- | Personal game configuration file type definitions.-module Game.LambdaHack.Config- ( Config(..), ConfigUI(..), FovMode(..)- ) where--import Data.Text (Text)-import Data.Binary--import qualified Game.LambdaHack.Key as K---- 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)---- | Fully typed contents of the rules config file.-data Config = Config- { configSelfString :: !String- -- caves- , configCaves :: ![(Text, Text)]- -- dungeon- , configDepth :: !Int- -- engine- , configFovMode :: !FovMode- , configSmellTimeout :: !Int- -- heroes- , configBaseHP :: !Int- , configExtraHeroes :: !Int- , configFirstDeathEnds :: !Bool- , configFaction :: !Text- } deriving Show---- | Fully typed contents of the UI config file.-data ConfigUI = ConfigUI- { -- commands- configCommands :: ![(K.Key, String)] -- TODO: define Binary Cmd- -- files- , configAppDataDir :: !FilePath- , configDiaryFile :: !FilePath- , configSaveFile :: !FilePath- , configBkpFile :: !FilePath- , configScoresFile :: !FilePath- , configRulesCfgFile :: !FilePath- , configUICfgFile :: !FilePath- -- heroNames- , configHeroNames :: ![(Int, Text)]- -- macros- , configMacros :: ![(K.Key, K.Key)]- -- ui- , configFont :: !String- , configHistoryMax :: !Int- } deriving Show--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)"--instance Binary Config where- put Config{..} = do- put configSelfString- put configCaves- put configDepth- put configFovMode- put configSmellTimeout- put configBaseHP- put configExtraHeroes- put configFirstDeathEnds- put configFaction- get = do- configSelfString <- get- configCaves <- get- configDepth <- get- configFovMode <- get- configSmellTimeout <- get- configBaseHP <- get- configExtraHeroes <- get- configFirstDeathEnds <- get- configFaction <- get- return Config{..}--instance Binary ConfigUI where- put ConfigUI{..} = do- put configCommands- put configAppDataDir- put configDiaryFile- put configSaveFile- put configBkpFile- put configRulesCfgFile- put configUICfgFile- put configScoresFile- put configHeroNames- put configMacros- put configFont- put configHistoryMax- get = do- configCommands <- get- configAppDataDir <- get- configDiaryFile <- get- configSaveFile <- get- configBkpFile <- get- configScoresFile <- get- configRulesCfgFile <- get- configUICfgFile <- get- configHeroNames <- get- configMacros <- get- configFont <- get- configHistoryMax <- get- return ConfigUI{..}
Game/LambdaHack/Content/ActorKind.hs view
@@ -3,15 +3,16 @@ ( ActorKind(..), avalidate ) where +import Control.Arrow ((&&&)) import qualified Data.List as L import qualified Data.Ord as Ord import Data.Text (Text) -import Game.LambdaHack.Ability-import Game.LambdaHack.Color-import qualified Game.LambdaHack.Random as Random-import Game.LambdaHack.Misc-import Game.LambdaHack.Time+import Game.LambdaHack.Common.Ability+import Game.LambdaHack.Common.Color+import Game.LambdaHack.Common.Misc+import qualified Game.LambdaHack.Common.Random as Random+import Game.LambdaHack.Common.Time -- TODO: make all but a few fields optional in some way, so that, a.g., -- a game content with no regeneration does not ever need to mention aregen.@@ -36,7 +37,7 @@ -- Make sure actor kinds can be told apart on the level map. avalidate :: [ActorKind] -> [ActorKind] avalidate l =- let cmp = Ord.comparing (\ ka -> (asymbol ka, acolor ka))+ let cmp = Ord.comparing $ asymbol &&& acolor eq ka1 ka2 = cmp ka1 ka2 == Ord.EQ sorted = L.sortBy cmp l nubbed = L.nubBy eq sorted
Game/LambdaHack/Content/CaveKind.hs view
@@ -7,34 +7,33 @@ import Data.Text (Text) import qualified Data.Text as T -import Game.LambdaHack.PointXY-import Game.LambdaHack.Random-import Game.LambdaHack.Misc+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random -- | Parameters for the generation of dungeon levels. data CaveKind = CaveKind- { csymbol :: Char -- ^ a symbol- , cname :: Text -- ^ short description- , cfreq :: Freqs -- ^ frequency within groups- , cxsize :: X -- ^ X size of the whole cave- , cysize :: Y -- ^ Y size of the whole cave- , cgrid :: RollDiceXY -- ^ the dimensions of the grid of places- , cminPlaceSize :: RollDiceXY -- ^ minimal size of places- , cdarkChance :: RollDeep -- ^ the chance a place is dark- , cauxConnects :: Rational -- ^ a proportion of extra connections- , cvoidChance :: Chance -- ^ the chance of not creating a place- , cnonVoidMin :: Int -- ^ extra places, may overlap except two- , cminStairDist :: Int -- ^ minimal distance between stairs- , cdoorChance :: Chance -- ^ the chance of a door in an opening- , copenChance :: Chance -- ^ if there's a door, is it open?- , chiddenChance :: Chance -- ^ if not open, is it hidden?- , citemNum :: RollDice -- ^ the number of items in the cave- , cdefaultTile :: Text -- ^ the default cave tile group name- , ccorridorTile :: Text -- ^ the cave corridor tile group name- , cfillerTile :: Text -- ^ the filler wall group name- , cdarkLegendTile :: Text -- ^ the dark place plan legend ground name- , clitLegendTile :: Text -- ^ the lit place plan legend ground name- , chiddenTile :: Text -- ^ the hidden tiles ground name+ { csymbol :: Char -- ^ a symbol+ , cname :: Text -- ^ short description+ , cfreq :: Freqs -- ^ frequency within groups+ , cxsize :: X -- ^ X size of the whole cave+ , cysize :: Y -- ^ Y size of the whole cave+ , cgrid :: RollDiceXY -- ^ the dimensions of the grid of places+ , cminPlaceSize :: RollDiceXY -- ^ minimal size of places+ , cdarkChance :: RollDeep -- ^ the chance a place is dark+ , cauxConnects :: Rational -- ^ a proportion of extra connections+ , cvoidChance :: Chance -- ^ the chance of not creating a place+ , cnonVoidMin :: Int -- ^ extra places, may overlap except two+ , cminStairDist :: Int -- ^ minimal distance between stairs+ , cdoorChance :: Chance -- ^ the chance of a door in an opening+ , copenChance :: Chance -- ^ if there's a door, is it open?+ , chidden :: Int -- ^ if not open, hidden one in n times+ , citemNum :: RollDice -- ^ the number of items in the cave+ , cdefTile :: Text -- ^ the default cave tile group name+ , ccorridorTile :: Text -- ^ the cave corridor tile group name+ , cfillerTile :: Text -- ^ the filler wall group name+ , cdarkLegendTile :: Text -- ^ the dark place plan legend ground name+ , clitLegendTile :: Text -- ^ the lit place plan legend ground name } deriving Show -- No Eq and Ord to make extending it logically sound, see #53
Game/LambdaHack/Content/FactionKind.hs view
@@ -3,18 +3,16 @@ ( FactionKind(..), fvalidate ) where -import Game.LambdaHack.Misc import Data.Text (Text)+import Game.LambdaHack.Common.Misc -- | Faction properties that are fixed for a given kind of factions. data FactionKind = FactionKind- { fsymbol :: !Char -- ^ a symbol- , fname :: !Text -- ^ short description- , ffreq :: !Freqs -- ^ frequency within groups- , fAiSelected :: !Text -- ^ Ai to use for the selected actor- , fAiIdle :: !Text -- ^ Ai to use for idle actors- , fenemy :: ![Text] -- ^ initially in war with these factions- , fally :: ![Text] -- ^ initially allied with these factions+ { fsymbol :: !Char -- ^ a symbol+ , fname :: !Text -- ^ short description+ , ffreq :: !Freqs -- ^ frequency within groups+ , fAiLeader :: !Text -- ^ AI to use for the selected actor+ , fAiMember :: !Text -- ^ AI to use for idle actors } deriving Show
Game/LambdaHack/Content/ItemKind.hs view
@@ -7,32 +7,23 @@ import Data.Text (Text) import qualified NLP.Miniutter.English as MU -import Game.LambdaHack.Effect-import Game.LambdaHack.Flavour-import Game.LambdaHack.Random-import Game.LambdaHack.Misc+import Game.LambdaHack.Common.Effect+import Game.LambdaHack.Common.Flavour+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Random --- TODO: ipower is out of place here. It doesn't make sense for all items,--- and will mean different things for different items. Perhaps it should--- be part of the Effect, but then we have to be careful to distinguish--- parts of the Effect that are rolled on item creation and those rolled--- at each use (e.g., sword magical +damage vs. sword damage dice).--- Another thing to keep in minds is that ipower will heavily determine--- the value of the item for shops, treasure chests, artifact set rebalancing,--- etc., so if we make ipower complex, the value computation gets complex too. -- | Item properties that are fixed for a given kind of items. data ItemKind = ItemKind- { isymbol :: !Char -- ^ map symbol- , iname :: !Text -- ^ generic name- , ifreq :: !Freqs -- ^ frequency within groups- , iflavour :: ![Flavour] -- ^ possible flavours- , ieffect :: !Effect -- ^ the effect when activated- , icount :: !RollDeep -- ^ created in that quantify- , ipower :: !RollDeep -- ^ created with that power+ { 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 possibly combat , iverbProject :: !MU.Part -- ^ the verb for projecting- , iweight :: !Int -- ^ weight in grams- , itoThrow :: !Int -- ^ percentage bonus or malus to throw speed+ , iweight :: !Int -- ^ weight in grams+ , itoThrow :: !Int -- ^ percentage bonus or malus to throw speed } deriving Show -- No Eq and Ord to make extending it logically sound, see #53
Game/LambdaHack/Content/PlaceKind.hs view
@@ -7,7 +7,7 @@ import Data.Text (Text) import qualified Data.Text as T -import Game.LambdaHack.Misc+import Game.LambdaHack.Common.Misc -- | Parameters for the generation of small areas within a dungeon level. data PlaceKind = PlaceKind
Game/LambdaHack/Content/RuleKind.hs view
@@ -3,13 +3,13 @@ ( RuleKind(..), ruvalidate ) where -import Data.Version import Data.Text (Text)+import Data.Version -import Game.LambdaHack.PointXY import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.Point-import Game.LambdaHack.Misc+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.PointXY -- TODO: very few rules are configurable yet, extend as needed. @@ -24,8 +24,8 @@ -- as for the other content. -- -- The @raccessible@ field holds a predicate that tells--- whether one location is accessible from another.--- Precondition: the two locations are next to each other.+-- whether one position is accessible from another.+-- Precondition: the two positions are next to each other. data RuleKind = RuleKind { rsymbol :: Char -- ^ a symbol , rname :: Text -- ^ short description
Game/LambdaHack/Content/StrategyKind.hs view
@@ -5,8 +5,8 @@ import Data.Text (Text) -import Game.LambdaHack.Ability-import Game.LambdaHack.Misc+import Game.LambdaHack.Common.Ability+import Game.LambdaHack.Common.Misc -- | Strategy properties that are fixed for a given kind of strategies. data StrategyKind = StrategyKind
Game/LambdaHack/Content/TileKind.hs view
@@ -4,13 +4,13 @@ ) where import qualified Data.List as L-import qualified Data.Map as M+import qualified Data.Map.Strict as M import Data.Text (Text) +import Game.LambdaHack.Common.Color+import Game.LambdaHack.Common.Feature+import Game.LambdaHack.Common.Misc import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Color-import Game.LambdaHack.Feature-import Game.LambdaHack.Misc -- | The type of kinds of terrain tiles. See @Tile.hs@ for explanation -- of the absence of a corresponding type @Tile@ that would hold@@ -36,8 +36,12 @@ -- wrt dungeon generation, AI preferences, etc.). tvalidate :: [TileKind] -> [TileKind] tvalidate lt =- let listFov f = L.map (\ kt -> ((tsymbol kt, f kt), [kt])) lt- mapFov :: (TileKind -> Color) -> M.Map (Char, Color) [TileKind]+ let listFov f = L.map (\ kt -> ( ( tsymbol kt+ , 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` lt namesUnequal (hd : tl) = let name = tname hd
− Game/LambdaHack/Draw.hs
@@ -1,183 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Display game data on the screen using one of the available frontends--- (determined at compile time with cabal flags).-{-# LANGUAGE CPP #-}-module Game.LambdaHack.Draw- ( ColorMode(..), draw, animate- ) where--import qualified Data.IntSet as IS-import qualified Data.List as L-import qualified Data.IntMap as IM-import Data.Maybe-import Data.Text (Text)-import qualified Data.Text as T--import Game.LambdaHack.Msg-import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.Animation (SingleFrame(..), Animation, rederAnim)-import Game.LambdaHack.State-import Game.LambdaHack.PointXY-import Game.LambdaHack.Point-import Game.LambdaHack.Vector-import Game.LambdaHack.Level-import Game.LambdaHack.Effect-import Game.LambdaHack.Perception-import Game.LambdaHack.Actor as Actor-import Game.LambdaHack.ActorState-import qualified Game.LambdaHack.Dungeon as Dungeon-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.Content.ItemKind-import qualified Game.LambdaHack.Item as Item-import Game.LambdaHack.Random-import qualified Game.LambdaHack.Kind as Kind-import qualified Game.LambdaHack.Feature as F-import Game.LambdaHack.Time-import Game.LambdaHack.Config---- | Color mode for the display.-data ColorMode =- ColorFull -- ^ normal, with full colours- | 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 -> State -> Overlay- -> SingleFrame-draw dm cops per s@State{ scursor=Cursor{..}- , sflavour, slid, splayer, sdebug- } overlay =- let Kind.COps{ coactor=Kind.Ops{okind}- , coitem=coitem@Kind.Ops{okind=iokind}- , cotile=Kind.Ops{okind=tokind, ouniqGroup} } = cops- DebugMode{smarkVision, somniscient} = sdebug- lvl@Level{lxsize, lysize, lsmell, ldesc, lactor, ltime, lclear, lseen} =- slevel s- (_, mpl@Actor{bkind, bhp, bloc}, bitems) = findActorAnyLevel splayer s- ActorKind{ahp, asmell} = okind bkind- reachable = debugTotalReachable per- visible = totalVisible per- (msgTop, over, msgBottom) = stringByLocation lxsize lysize overlay- (sSml, sVis) = case smarkVision of- Just Blind -> (True, True)- Just _ -> (False, True)- Nothing | asmell -> (True, False)- Nothing -> (False, False)- lAt = if somniscient then at else rememberAt- liAt = if somniscient then atI else rememberAtI- sVisBG = if sVis- then \ vis rea -> if vis- then Color.Blue- else if rea- then Color.Magenta- else Color.defBG- else \ _vis _rea -> Color.defBG- (_, wealth) = calculateTotal coitem s- damage = case Item.strongestSword cops bitems of- Just sw -> case ieffect $ iokind $ Item.jkind sw of- Wound dice -> showT dice <> "+" <> showT (Item.jpower sw)- _ -> showT (Item.jpower sw)- Nothing -> "3d1" -- TODO; use the item 'fist'- bl = fromMaybe [] $ bla lxsize lysize ceps bloc clocation- dis pxy =- let loc0 = toPoint lxsize pxy- tile = lvl `lAt` loc0- tk = tokind tile- items = lvl `liAt` loc0- sml = IM.findWithDefault timeZero loc0 lsmell- smlt = sml `timeAdd` timeNegate ltime- viewActor loc Actor{bkind = bkind2, bsymbol, bcolor}- | loc == bloc && slid == creturnLn =- (symbol, Color.defBG) -- highlight player- | otherwise = (symbol, color)- where- ActorKind{asymbol, acolor} = okind bkind2- color = fromMaybe acolor bcolor- symbol = fromMaybe asymbol bsymbol- rainbow loc = toEnum $ loc `rem` 14 + 1- actorsHere = IM.elems lactor- (char, fg0) =- case ( L.find (\ m -> loc0 == Actor.bloc m) actorsHere- , L.find (\ m -> clocation == Actor.bloc m) actorsHere ) of- (_, actorTgt) | ctargeting /= TgtOff- && (slid == creturnLn- && L.elem loc0 bl- || (case actorTgt of- Just (Actor{ btarget=TPath p- , bloc=prLoc }) ->- L.elem loc0 $ shiftPath prLoc 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 m, _) | somniscient || vis -> viewActor loc0 m- _ | sSml && smlt > timeZero ->- (timeToDigit (smellTimeout s) smlt, rainbow loc0)- | otherwise ->- case items of- [] -> (tsymbol tk, if vis then tcolor tk else tcolor2 tk)- i : _ -> Item.viewItem coitem (Item.jkind i) sflavour- vis = IS.member loc0 visible- rea = IS.member loc0 reachable- bg0 = if ctargeting /= TgtOff && loc0 == clocation- then Color.defFG -- highlight target cursor- else sVisBG vis rea -- FOV debug or standard bg- reverseVideo = Color.Attr{ fg = Color.bg Color.defaultAttr- , bg = Color.fg Color.defaultAttr- }- optVisually attr@Color.Attr{fg, bg} =- if (fg == Color.defBG)- || (bg == Color.defFG && fg == Color.defFG)- then reverseVideo- else attr- a = case dm of- ColorBW -> Color.defaultAttr- ColorFull -> optVisually Color.Attr{fg = fg0, bg = bg0}- in case over pxy of- Just c -> Color.AttrChar Color.defaultAttr c- _ -> Color.AttrChar a char- seenN = 100 * lseen `div` lclear- seenTxt | seenN == 100 = "all"- | otherwise = T.justifyRight 2 ' ' (showT seenN) <> "%"- -- Indicate the actor is braced (was waiting last move).- -- It's a useful feedback for the otherwise hard to observe- -- 'wait' command.- braceSign | braced mpl ltime = "{"- | otherwise = " "- lvlN = T.justifyLeft 2 ' ' (showT $ Dungeon.levelNumber slid)- stats =- T.justifyLeft 11 ' ' ("[" <> seenTxt <+> "seen]") <+>- T.justifyLeft 9 ' ' ("$:" <+> showT wealth) <+>- T.justifyLeft 11 ' ' ("Dmg:" <+> damage) <+>- T.justifyLeft 13 ' ' (braceSign <> "HP:" <+> showT bhp- <+> "(" <> showT (maxDice ahp) <> ")")- widthForDesc = lxsize - T.length stats - T.length lvlN - 3- status = lvlN <+> T.justifyLeft widthForDesc ' ' ldesc <+> stats- toWidth :: Int -> Text -> Text- toWidth n x = T.take n (T.justifyLeft n ' ' x)- fLine y =- let f l x = let !ac = dis (PointXY (x, y)) in ac : l- in L.foldl' f [] [lxsize-1,lxsize-2..0]- sfLevel = -- Fully evaluated.- let f l y = let !line = fLine y in line : l- in L.foldl' f [] [lysize-1,lysize-2..0]- sfTop = toWidth lxsize $ msgTop- sfBottom = toWidth (lxsize - 1) $ fromMaybe status $ msgBottom- in SingleFrame{..}---- | Render animations on top of the current screen frame.-animate :: State -> Diary -> Kind.COps -> Perception -> Animation- -> [Maybe SingleFrame]-animate s Diary{sreport} cops per anim =- let Level{lxsize, lysize} = slevel s- over = renderReport sreport- topLineOnly = padMsg lxsize over- basicFrame = draw ColorFull cops per s [topLineOnly]- in rederAnim lxsize lysize basicFrame anim
− Game/LambdaHack/Dungeon.hs
@@ -1,85 +0,0 @@--- | The game arena comprised of levels. No operation in this module--- involves the 'State', 'COps', 'Config' or 'Action' type.-module Game.LambdaHack.Dungeon- ( -- * Level identifier- LevelId, levelNumber, levelDefault- -- * Dungeon- , Dungeon, fromList, currentFirst, adjust, mapDungeon, (!), lookup, depth- ) where--import Prelude hiding (lookup)-import Data.Binary-import qualified Data.Map as M-import qualified Data.List as L--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Level---- | Level ids are, for now, ordered linearly by depth.-newtype LevelId = LambdaCave Int- deriving (Show, Eq, Ord)--instance Binary LevelId where- put (LambdaCave n) = put n- get = fmap LambdaCave get---- | Depth of a level.-levelNumber :: LevelId -> Int-levelNumber (LambdaCave n) = n---- | Default level for a given depth.-levelDefault :: Int -> LevelId-levelDefault = LambdaCave---- | The complete dungeon is a map from level names to levels.-data Dungeon = Dungeon- { dungeonLevelMap :: M.Map LevelId Level- , dungeonDepth :: Int -- can be different than the number of levels- }- deriving Show--instance Binary Dungeon where- put Dungeon{..} = do- put (M.toAscList dungeonLevelMap)- put dungeonDepth- get = do- lvls <- get- let dungeonLevelMap = M.fromDistinctAscList lvls- dungeonDepth <- get- return Dungeon{..}---- | Create a dungeon from a list of levels and maximum depth.--- The depth is only a danger indicator;--- there may potentially be multiple levels--- with the same depth.-fromList :: [(LevelId, Level)] -> Int -> Dungeon-fromList lvls d = assert (d <= L.length lvls `blame` (d, L.length lvls)) $- Dungeon (M.fromList lvls) d---- | Association list corresponding to the dungeon.--- Starts at the supplied level id (usually the current level)--- to try to speed up the searches and keep the dungeon lazy.-currentFirst :: LevelId -> Dungeon -> [(LevelId, Level)]-currentFirst lid (Dungeon m _) =- (lid, m M.! lid)- : L.filter ((/= lid) . fst) (M.assocs m)---- | Adjust the level at a given id.-adjust :: (Level -> Level) -> LevelId -> Dungeon -> Dungeon-adjust f lid (Dungeon m d) = Dungeon (M.adjust f lid m) d---- | Adjust the level at a given id.-mapDungeon :: (Level -> Level) -> Dungeon -> Dungeon-mapDungeon f (Dungeon m d) = Dungeon (M.map f m) d---- | Find a level with the given id.-(!) :: Dungeon -> LevelId -> Level-(!) (Dungeon m _) lid = m M.! lid---- | Try to look up a level with the given id.-lookup :: LevelId -> Dungeon -> Maybe Level-lookup lid (Dungeon m _) = M.lookup lid m---- | Maximum depth of the dungeon.-depth :: Dungeon -> Int-depth = dungeonDepth
− Game/LambdaHack/DungeonState.hs
@@ -1,182 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Dungeon operations that require 'State', 'Kind.COps'--- or 'Config.Config' type.-module Game.LambdaHack.DungeonState- ( -- * Dungeon generation- FreshDungeon(..), generate- -- * Dungeon travel- , whereTo- ) where--import qualified System.Random as R-import qualified Data.List as L-import qualified Control.Monad.State as MState-import qualified Data.Map as M-import qualified Data.IntMap as IM-import Data.Maybe-import Control.Monad-import Data.Text (Text)--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Point-import Game.LambdaHack.Level-import qualified Game.LambdaHack.Dungeon as Dungeon-import Game.LambdaHack.Random-import Game.LambdaHack.Config-import Game.LambdaHack.State-import qualified Game.LambdaHack.Feature as F-import qualified Game.LambdaHack.Tile as Tile-import Game.LambdaHack.Content.CaveKind-import Game.LambdaHack.Cave hiding (TileMapXY)-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Item-import Game.LambdaHack.PointXY-import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.Place-import qualified Game.LambdaHack.Effect as Effect-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Time-import Game.LambdaHack.Msg--convertTileMaps :: Rnd (Kind.Id TileKind) -> Int -> Int -> TileMapXY- -> Rnd TileMap-convertTileMaps cdefaultTile cxsize cysize lmap = do- let bounds = (origin, toPoint cxsize $ PointXY (cxsize - 1, cysize - 1))- assocs = map (\ (xy, t) -> (toPoint cxsize xy, t)) (M.assocs lmap)- pickedTiles <- replicateM (cxsize * cysize) cdefaultTile- return $ Kind.listArray bounds pickedTiles Kind.// assocs--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)--mapToIMap :: X -> M.Map PointXY a -> IM.IntMap a-mapToIMap cxsize m =- IM.fromList $ map (\ (xy, a) -> (toPoint cxsize xy, a)) (M.assocs m)--rollItems :: Kind.COps -> Int -> Int -> CaveKind -> TileMap -> Point- -> Rnd [(Point, Item)]-rollItems Kind.COps{cotile, coitem=coitem@Kind.Ops{okind}}- ln depth CaveKind{cxsize, citemNum, cminStairDist} lmap ploc = do- nri <- rollDice citemNum- replicateM nri $ do- item <- newItem coitem ln depth- let ik = okind (jkind item)- l <- case ieffect ik of- Effect.Wound dice | maxDice dice > 0 -- a weapon- && maxDice dice + maxDeep (ipower ik) > 3 ->- -- Powerful weapons generated close to monsters, MUAHAHAHA.- findLocTry 20 lmap -- 20 only, for unpredictability- [ \ l _ -> chessDist cxsize ploc l > cminStairDist- , \ l _ -> chessDist cxsize ploc l > 2 * cminStairDist `div` 3- , \ l _ -> chessDist cxsize ploc l > cminStairDist `div` 2- , \ l _ -> chessDist cxsize ploc l > cminStairDist `div` 3- , const (Tile.hasFeature cotile F.Boring)- ]- _ -> findLoc lmap (const (Tile.hasFeature cotile F.Boring))- return (l, item)--placeStairs :: Kind.Ops TileKind -> TileMap -> CaveKind -> [Place]- -> Rnd (Point, Kind.Id TileKind, Point, Kind.Id TileKind)-placeStairs cotile@Kind.Ops{opick} cmap CaveKind{..} dplaces = do- su <- findLoc cmap (const (Tile.hasFeature cotile F.Boring))- sd <- findLocTry 1000 cmap- [ \ l _ -> chessDist cxsize su l >= cminStairDist- , \ l _ -> chessDist cxsize su l >= cminStairDist `div` 2- , \ l t -> l /= su && Tile.hasFeature cotile F.Boring t- ]- let fitArea loc = inside cxsize loc . qarea- findLegend loc =- maybe clitLegendTile qlegend $ L.find (fitArea loc) dplaces- upId <- opick (findLegend su) $ Tile.kindHasFeature F.Ascendable- downId <- opick (findLegend sd) $ Tile.kindHasFeature F.Descendable- return (su, upId, sd, downId)---- | Create a level from a cave, from a cave kind.-buildLevel :: Kind.COps -> Cave -> Int -> Int -> Rnd Level-buildLevel cops@Kind.COps{ cotile=cotile@Kind.Ops{opick, ouniqGroup}- , cocave=Kind.Ops{okind} }- Cave{..} ln depth = do- let kc@CaveKind{..} = okind dkind- cmap <- convertTileMaps (opick cdefaultTile (const True)) cxsize cysize dmap- (su, upId, sd, downId) <-- placeStairs cotile cmap kc dplaces- let stairs = (su, upId) : if ln == depth then [] else [(sd, downId)]- lmap = cmap Kind.// stairs- f !n !tk | Tile.isExplorable cotile tk = n + 1- | otherwise = n- lclear = Kind.foldlArray f 0 lmap- is <- rollItems cops ln depth kc lmap su- -- TODO: split this into Level.defaultLevel- let itemMap = mapToIMap cxsize ditem `IM.union` IM.fromList is- litem = IM.map (\ i -> ([i], [])) itemMap- unknownId = ouniqGroup "unknown space"- level = Level- { lactor = IM.empty- , linv = IM.empty- , lxsize = cxsize- , lysize = cysize- , lsmell = IM.empty- , lsecret = mapToIMap cxsize dsecret- , litem- , lmap- , lrmap = unknownTileMap unknownId cxsize cysize- , ldesc = cname- , lmeta = dmeta- , lstairs = (su, sd)- , ltime = timeTurn- , lclear- , lseen = 0- }- return level--matchGenerator :: Kind.Ops CaveKind -> Maybe Text -> Rnd (Kind.Id CaveKind)-matchGenerator Kind.Ops{opick} mname =- opick (fromMaybe "dng" mname) (const True)--findGenerator :: Kind.COps -> Config -> Int -> Int -> Rnd Level-findGenerator cops Config{configCaves} k depth = do- let ln = "LambdaCave_" <> showT k- genName = L.lookup ln configCaves- ci <- matchGenerator (Kind.cocave cops) genName- cave <- buildCave cops k depth ci- buildLevel cops cave k depth---- | Freshly generated and not yet populated dungeon.-data FreshDungeon = FreshDungeon- { entryLevel :: Dungeon.LevelId -- ^ starting level for the party- , entryLoc :: Point -- ^ starting location for the party- , freshDungeon :: Dungeon.Dungeon -- ^ level maps- }---- | Generate the dungeon for a new game.-generate :: Kind.COps -> Config -> Rnd FreshDungeon-generate cops config@Config{configDepth} =- let gen :: R.StdGen -> Int -> (R.StdGen, (Dungeon.LevelId, Level))- gen g k =- let (g1, g2) = R.split g- res = MState.evalState (findGenerator cops config k configDepth) g1- in (g2, (Dungeon.levelDefault k, res))- con :: R.StdGen -> (FreshDungeon, R.StdGen)- con g = assert (configDepth >= 1 `blame` configDepth) $- let (gd, levels) = L.mapAccumL gen g [1..configDepth]- entryLevel = Dungeon.levelDefault 1- entryLoc = fst (lstairs (snd (head levels)))- freshDungeon = Dungeon.fromList levels configDepth- in (FreshDungeon{..}, gd)- in MState.state con---- | Compute the level identifier and starting location on the level,--- after a level change.-whereTo :: State -- ^ game state- -> Int -- ^ jump this many levels- -> Maybe (Dungeon.LevelId, Point)- -- ^ target level and the location of its receiving stairs-whereTo State{slid, sdungeon} k = assert (k /= 0) $- let n = Dungeon.levelNumber slid- nln = n - k- ln = Dungeon.levelDefault nln- in case Dungeon.lookup ln sdungeon of- Nothing -> Nothing- Just lvlTrg -> Just (ln, (if k < 0 then fst else snd) (lstairs lvlTrg))
− Game/LambdaHack/Effect.hs
@@ -1,61 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Effects of content on other content. No operation in this module--- involves the 'State' or 'Action' type.-module Game.LambdaHack.Effect- ( Effect(..), effectToSuffix, effectToBenefit- ) where--import Data.Text (Text)--import Game.LambdaHack.Random-import Game.LambdaHack.Msg---- TODO: document each constructor--- | All possible effects, some of them parameterized or dependent--- on outside coefficients, e.g., item power.-data Effect =- NoEffect- | Heal -- healing strength in ipower- | Wound !RollDice -- base damage, to-dam bonus in ipower- | Dominate- | SummonFriend- | SummonEnemy- | ApplyPerfume- | Regeneration- | Searching- | Ascend- | Descend- deriving (Show, Read, Eq, Ord)---- | Suffix to append to a basic content name, if the content causes the effect.-effectToSuffix :: Effect -> Text-effectToSuffix NoEffect = ""-effectToSuffix Heal = "of healing"-effectToSuffix (Wound dice@(RollDice a b)) =- if a == 0 && b == 0- then "of wounding"- else "(" <> showT dice <> ")"-effectToSuffix Dominate = "of domination"-effectToSuffix SummonFriend = "of aid calling"-effectToSuffix SummonEnemy = "of summoning"-effectToSuffix ApplyPerfume = "of rose water"-effectToSuffix Regeneration = "of regeneration"-effectToSuffix Searching = "of searching"-effectToSuffix Ascend = "of ascending"-effectToSuffix Descend = "of descending"---- | How much AI benefits from applying the effect. Multipllied by item power.--- Negative means harm to the enemy when thrown at him. Effects with zero--- benefit won't ever be used, neither actively nor passively.-effectToBenefit :: Effect -> Int-effectToBenefit NoEffect = 0-effectToBenefit Heal = 10 -- TODO: depends on (maxhp - hp)-effectToBenefit (Wound _) = -10 -- TODO: dice ignored for now-effectToBenefit Dominate = 0 -- AI can't use this-effectToBenefit SummonFriend = 100-effectToBenefit SummonEnemy = 0-effectToBenefit ApplyPerfume = 0-effectToBenefit Regeneration = 0 -- much more benefit from carrying around-effectToBenefit Searching = 0 -- AI does not need to search-effectToBenefit Ascend = 0 -- AI can't change levels-effectToBenefit Descend = 0
− Game/LambdaHack/EffectAction.hs
@@ -1,625 +0,0 @@-{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}-{-# OPTIONS_GHC -fno-warn-type-defaults #-}--- | The effectToAction function and all it depends on.--- This file should not depend on Actions.hs nor ItemAction.hs.--- TODO: Add an export list and document after it's rewritten according to #17.-module Game.LambdaHack.EffectAction where--import Control.Monad-import Control.Monad.State hiding (State, state)-import Data.Function-import Data.Maybe-import qualified Data.List as L-import qualified Data.IntMap as IM-import qualified Data.Set as S-import qualified Data.IntSet as IS-import Data.Monoid (mempty)-import Data.Text (Text)-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Action-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Draw-import Game.LambdaHack.Point-import Game.LambdaHack.Item-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Level-import Game.LambdaHack.Misc-import Game.LambdaHack.Msg-import Game.LambdaHack.Perception-import Game.LambdaHack.Random-import Game.LambdaHack.State-import Game.LambdaHack.Time-import Game.LambdaHack.Config-import qualified Game.LambdaHack.Effect as Effect-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.DungeonState-import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.Animation (twirlSplash, blockHit, deathBody)-import qualified Game.LambdaHack.Dungeon as Dungeon--default (Text)---- | Sentences such as \"Dog barks loudly.\"-actorVerb :: Kind.Ops ActorKind -> Actor -> Text -> Text-actorVerb coactor a v =- makeSentence [MU.SubjectVerbSg (partActor coactor a) (MU.Text v)]---- | Invoke pseudo-random computation with the generator kept in the state.-rndToAction :: Rnd a -> Action a-rndToAction r = do- g <- gets srandom- let (a, ng) = runState r g- modify (\ state -> state {srandom = ng})- return a---- | Update actor stats. Works for actors on other levels, too.-updateAnyActor :: ActorId -> (Actor -> Actor) -> Action ()-updateAnyActor actor f = modify (updateAnyActorBody actor f)---- | Update player-controlled actor stats.-updatePlayerBody :: (Actor -> Actor) -> Action ()-updatePlayerBody f = do- pl <- gets splayer- updateAnyActor pl f---- TODO: instead of verbosity return msg components and tailor them outside?--- TODO: separately define messages for the case when source == target--- and for the other case; then use the messages outside of effectToAction,--- depending on the returned bool, perception and identity of the actors.---- | The source actor affects the target actor, with a given effect and power.--- The second argument is verbosity of the resulting message.--- Both actors are on the current level and can be the same actor.--- The first bool result indicates if the effect was spectacular enough--- for the actors to identify it (and the item that caused it, if any).--- The second bool tells if the effect was seen by or affected the party.-effectToAction :: Effect.Effect -> Int -> ActorId -> ActorId -> Int -> Bool- -> Action (Bool, Bool)-effectToAction effect verbosity source target power block = do- oldTm <- gets (getActor target)- let oldHP = bhp oldTm- (b, msg) <- eff effect verbosity source target power- s <- get- -- If the target killed outright by the effect (e.g., in a recursive call),- -- there's nothing left to do. TODO: hacky; aren't messages lost?- if not (memActor target s)- then return (b, False)- else do- sm <- gets (getActor source)- tm <- gets (getActor target)- per <- getPerception- pl <- gets splayer- let sloc = bloc sm- tloc = bloc tm- svisible = sloc `IS.member` totalVisible per- tvisible = tloc `IS.member` totalVisible per- newHP = bhp $ getActor target s- bb <-- if isAHero s source ||- isAHero s target ||- pl == source ||- pl == target ||- -- Target part of message shown below, so target visibility checked.- tvisible- then do- -- Party sees the effect or is affected by it.- msgAdd msg- -- Try to show an animation. Sometimes, e.g., when HP is unchaged,- -- the animation will not be shown.- cops <- getCOps- diary <- getDiary- let locs = (breturn tvisible tloc,- breturn svisible sloc)- anim | newHP > oldHP =- twirlSplash locs Color.BrBlue Color.Blue- | newHP < oldHP && block =- blockHit locs Color.BrRed Color.Red- | newHP < oldHP && not block =- twirlSplash locs Color.BrRed Color.Red- | otherwise = mempty- animFrs = animate s diary cops per anim- mapM_ displayFramePush $ Nothing : animFrs- return (b, True)- else do- -- Hidden, but if interesting then heard.- when b $ msgAdd "You hear some noises."- return (b, False)- -- Now kill the actor, if needed. For monsters, no "die" message- -- is shown below. It should have been shown in @eff@.- when (newHP <= 0) $ do- -- Place the actor's possessions on the map.- bitems <- gets (getActorItem target)- modify (updateLevel (dropItemsAt bitems tloc))- -- Clean bodies up.- if target == pl- then -- Kill the player and check game over.- checkPartyDeath- else -- Kill the enemy.- modify (deleteActor target)- return bb---- | The boolean part of the result says if the ation was interesting--- and the string part describes how the target reacted--- (not what the source did).-eff :: Effect.Effect -> Int -> ActorId -> ActorId -> Int- -> Action (Bool, Text)-eff Effect.NoEffect _ _ _ _ = nullEffect-eff Effect.Heal _ _source target power = do- Kind.COps{coactor=coactor@Kind.Ops{okind}} <- getCOps- let bhpMax m = maxDice (ahp $ okind $ bkind m)- tm <- gets (getActor target)- if bhp tm >= bhpMax tm || power <= 0- then nullEffect- else do- void $ focusIfOurs target- updateAnyActor target (addHp coactor power)- return (True, actorVerb coactor tm "feel better")-eff (Effect.Wound nDm) verbosity source target power = do- Kind.COps{coactor} <- getCOps- s <- get- n <- rndToAction $ rollDice nDm- if n + power <= 0 then nullEffect else do- void $ focusIfOurs target- pl <- gets splayer- tm <- gets (getActor target)- let newHP = bhp tm - n - power- msg- | newHP <= 0 =- if target == pl- then "" -- Handled later on in checkPartyDeath. Suspense.- else -- Not as important, so let the player read the message- -- about monster death while he watches the combat animation.- if isProjectile s target- then actorVerb coactor tm "drop down"- else actorVerb coactor tm "die"- | source == target = -- a potion of wounding, etc.- actorVerb coactor tm "feel wounded"- | verbosity <= 0 = ""- | target == pl =- actorVerb coactor tm $ "lose" <+> showT (n + power) <> "HP"- | otherwise = actorVerb coactor tm "hiss in pain"- updateAnyActor target $ \ m -> m { bhp = newHP } -- Damage the target.- return (True, msg)-eff Effect.Dominate _ source target _power = do- s <- get- if not $ isAHero s target- then do -- Monsters have weaker will than heroes.- selectPlayer target- >>= assert `trueM` (source, target, "player dominates himself")- -- Sync the monster with the hero move time for better display- -- of missiles and for the domination to actually take one player's turn.- updatePlayerBody (\ m -> m { btime = stime s})- -- Display status line and FOV for the newly controlled actor.- fr <- drawPrompt ColorBW ""- mapM_ displayFramePush [Nothing, Just fr, Nothing]- return (True, "")- else if source == target- then do- lm <- gets hostileList- lxsize <- gets (lxsize . slevel)- lysize <- gets (lysize . slevel)- let cross m = bloc m : vicinityCardinal lxsize lysize (bloc m)- vis = L.concatMap cross lm- rememberList vis- return (True, "A dozen voices yells in anger.")- else nullEffect-eff Effect.SummonFriend _ source target power = do- tm <- gets (getActor target)- s <- get- if isAHero s source- then summonHeroes (1 + power) (bloc tm)- else summonMonsters (1 + power) (bloc tm)- return (True, "")-eff Effect.SummonEnemy _ source target power = do- tm <- gets (getActor target)- s <- get- -- A trick: monster player summons a hero.- if isAHero s source- then summonMonsters (1 + power) (bloc tm)- else summonHeroes (1 + power) (bloc tm)- return (True, "")-eff Effect.ApplyPerfume _ source target _ =- if source == target- then return (True, "Tastes like water, but with a strong rose scent.")- else do- let upd lvl = lvl { lsmell = IM.empty }- modify (updateLevel upd)- return (True, "The fragrance quells all scents in the vicinity.")-eff Effect.Regeneration verbosity source target power =- eff Effect.Heal verbosity source target power-eff Effect.Searching _ _source _target _power =- return (True, "It gets lost and you search in vain.")-eff Effect.Ascend _ source target power = do- tm <- gets (getActor target)- s <- get- Kind.COps{coactor} <- getCOps- void $ focusIfOurs target- if not $ isAHero s target -- not target /= pl: to squash friendly monster- then squashActor source target- else effLvlGoUp (power + 1)- -- TODO: The following message too late if a monster squashed by going up,- -- unless it's ironic. ;) The same below.- s2 <- get- return $ if maybe Camping snd (squit s2) == Victor- then (True, "")- else (True, actorVerb coactor tm "find a way upstairs")-eff Effect.Descend _ source target power = do- tm <- gets (getActor target)- s <- get- Kind.COps{coactor} <- getCOps- void $ focusIfOurs target- if not $ isAHero s target- then squashActor source target- else effLvlGoUp (- (power + 1))- s2 <- get- return $ if maybe Camping snd (squit s2) == Victor- then (True, "")- else (True, actorVerb coactor tm "find a way downstairs")--nullEffect :: Action (Bool, Text)-nullEffect = return (False, "Nothing happens.")---- TODO: refactor with actorAttackActor.-squashActor :: ActorId -> ActorId -> Action ()-squashActor source target = do- Kind.COps{coactor, coitem=Kind.Ops{okind, ouniqGroup}} <- getCOps- sm <- gets (getActor source)- tm <- gets (getActor target)- let h2hKind = ouniqGroup "weight"- power = maxDeep $ ipower $ okind h2hKind- h2h = Item h2hKind power Nothing 1- verb = iverbApply $ okind h2hKind- msg = makeSentence- [ MU.SubjectVerbSg (partActor coactor sm) verb- , partActor coactor tm- , "in a staircase accident" ]- msgAdd msg- itemEffectAction 0 source target h2h False- s <- get- -- The monster has to be killed first, before we step there (same turn!).- assert (not (memActor target s) `blame` (source, target, "not killed")) $- return ()--effLvlGoUp :: Int -> Action ()-effLvlGoUp k = do- pbody <- gets getPlayerBody- pl <- gets splayer- slid <- gets slid- st <- get- cops <- getCOps- lvl <- gets slevel- case whereTo st k of- Nothing -> fleeDungeon -- we are at the "end" of the dungeon- Just (nln, nloc) ->- assert (nln /= slid `blame` (nln, "stairs looped")) $ do- bitems <- gets getPlayerItem- -- Remember the level (e.g., for a teleport via scroll on the floor).- remember- -- Remove the player from the old level.- modify (deleteActor pl)- hs <- gets heroList- -- Monsters hear that players not on the level. Cancel smell.- -- Reduces memory load and savefile size.- when (L.null hs) $- modify (updateLevel (updateSmell (const IM.empty)))- -- At this place the invariant that the player exists fails.- -- Change to the new level (invariant not needed).- switchLevel nln- -- The player can now be safely added to the new level.- modify (insertActor pl pbody)- modify (updateAnyActorItem pl (const bitems))- -- At this place the invariant is restored again.- inhabitants <- gets (locToActor nloc)- case inhabitants of- Nothing -> return ()--- Broken if the effect happens, e.g. via a scroll and abort is not enough.--- Just h | isAHero st h ->--- -- Bail out if a party member blocks the staircase.--- abortWith "somebody blocks the staircase"- Just m ->- -- Aquash an actor blocking the staircase.- -- This is not a duplication with the other calls to squashActor,- -- because here an inactive actor is squashed.- squashActor pl m- -- Verify the monster on the staircase died.- inhabitants2 <- gets (locToActor nloc)- when (isJust inhabitants2) $ assert `failure` inhabitants2- -- Land the player at the other end of the stairs.- updatePlayerBody (\ p -> p { bloc = nloc })- -- Change the level of the player recorded in cursor.- modify (updateCursor (\ c -> c { creturnLn = nln }))- -- The invariant "at most one actor on a tile" restored.- -- Create a backup of the savegame.- saveGameBkp- state <- get- msgAdd $ lookAt cops False True state lvl nloc ""---- | Change level and reset it's time and update the times of all actors.--- The player may be added to @lactor@ of the new level only after--- this operation is executed.-switchLevel :: Dungeon.LevelId -> Action ()-switchLevel nln = do- timeCurrent <- gets stime- slid <- gets slid- when (slid /= nln) $ do- -- Switch to the new level.- modify (\ s -> s {slid = nln})- timeLastVisited <- gets stime- let diff = timeAdd timeCurrent $ timeNegate timeLastVisited- when (diff /= timeZero) $ do- -- Reset the level time.- modify $ updateTime $ const timeCurrent- -- Update the times of all actors.- let upd m@Actor{btime} = m {btime = timeAdd btime diff}- modify (updateLevel (updateActorDict (IM.map upd)))---- | The player leaves the dungeon.-fleeDungeon :: Action ()-fleeDungeon = do- Kind.COps{coitem=coitem@Kind.Ops{oname, ouniqGroup}} <- getCOps- s <- get- go <- displayYesNo "This is the way out. Really leave now?"- recordHistory -- Prevent repeating the ending msgs.- when (not go) $ abortWith "Game resumed."- let (items, total) = calculateTotal coitem s- modify (\ st -> st {squit = Just (False, Victor)})- 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?"- when (not go1) $ abortWith "Brave soul!"- go2 <- displayMore ColorBW- "This time try to grab some loot before escape!"- when (not go2) $ abortWith "Here's your chance!"- else do- let currencyName = MU.Text $ oname $ ouniqGroup "currency"- winMsg = makePhrase- [ "Congratulations, you won!"- , "Here's your loot, worth"- , MU.NWs total currencyName- , "." ]- io <- itemOverlay True True items- tryIgnore $ displayOverAbort winMsg io- modify (\ st -> st {squit = Just (True, Victor)})---- | The source actor affects the target actor, with a given item.--- If the event is seen, the item may get identified.-itemEffectAction :: Int -> ActorId -> ActorId -> Item -> Bool -> Action ()-itemEffectAction verbosity source target item block = do- Kind.COps{coitem=Kind.Ops{okind}} <- getCOps- st <- get- slidOld <- gets slid- let effect = ieffect $ okind $ jkind item- -- The msg describes the target part of the action.- (b1, b2) <- effectToAction effect verbosity source target (jpower item) block- -- Party sees or affected, and the effect interesting,- -- so the item gets identified.- when (b1 && b2) $ discover item- -- Destroys attacking actor and its items: a hack for projectiles.- slidNew <- gets slid- modify (\ s -> s {slid = slidOld})- when (isProjectile st source) $- modify (deleteActor source)- modify (\ s -> s {slid = slidNew})---- | Make the item known to the player.-discover :: Item -> Action ()-discover i = do- Kind.COps{coitem=coitem@Kind.Ops{okind}} <- getCOps- state <- get- let ik = jkind i- kind = okind ik- alreadyIdentified = L.length (iflavour kind) == 1- || ik `S.member` sdisco state- unless alreadyIdentified $ do- modify (updateDiscoveries (S.insert ik))- state2 <- get- let msg = makeSentence- [ "the", MU.SubjectVerbSg (partItem coitem state i) "turn out to be"- , MU.AW $ partItem coitem state2 i ]- msgAdd msg---- | Make the actor controlled by the player. Switch level, if needed.--- False, if nothing to do. Should only be invoked as a direct result--- of a player action or the selected player actor death.-selectPlayer :: ActorId -> Action Bool-selectPlayer actor = do- Kind.COps{coactor} <- getCOps- pl <- gets splayer- cops <- getCOps- lvl <- gets slevel- if actor == pl- then return False -- already selected- else do- state <- get- let (nln, pbody, _) = findActorAnyLevel actor state- -- Switch to the new level.- switchLevel nln- -- Make the new actor the player-controlled actor.- modify (\ s -> s {splayer = actor})- -- Record the original level of the new player.- modify (updateCursor (\ c -> c {creturnLn = nln}))- -- Don't continue an old run, if any.- stopRunning- -- Announce.- msgAdd $ makeSentence [partActor coactor pbody, "selected"]- msgAdd $ lookAt cops False True state lvl (bloc pbody) ""- return True---- TODO: center screen, flash the background, etc. Perhaps wait for SPACE.--- | Focus on the hero being wounded/displaced/etc.-focusIfOurs :: ActorId -> Action Bool-focusIfOurs target = do- s <- get- pl <- gets splayer- if isAHero s target || target == pl- then return True- else return False--summonHeroes :: Int -> Point -> Action ()-summonHeroes n loc =- assert (n > 0) $ do- cops <- getCOps- newHeroId <- gets scounter- configUI <- getConfigUI- modify (\ state -> iterate (addHero cops loc configUI) state !! n)- b <- focusIfOurs newHeroId- assert (b `blame` (newHeroId, "player summons himself")) $- return ()--summonMonsters :: Int -> Point -> Action ()-summonMonsters n loc = do- Kind.COps{ cotile- , coactor=Kind.Ops{opick, okind}- , cofact=Kind.Ops{opick=fopick, oname=foname}} <- getCOps- bfaction <- rndToAction $ fopick "spawn" (const True)- -- Spawn frequency required greater than zero, but otherwise ignored.- let inFaction m = isJust $ lookup (foname bfaction) (afreq m)- -- Summon frequency used for picking the actor.- mk <- rndToAction $ opick "summon" inFaction- hp <- rndToAction $ rollDice $ ahp $ okind mk- modify (\ s -> iterate (addMonster cotile mk hp loc- bfaction False) s !! n)---- | Remove dead heroes (or dead dominated monsters). Check if game is over.--- For now we only check the selected hero and at current level,--- but if poison, etc. is implemented, we'd need to check all heroes--- on any level.-checkPartyDeath :: Action ()-checkPartyDeath = do- cops@Kind.COps{coactor} <- getCOps- per <- getPerception- ahs <- gets allHeroesAnyLevel- pl <- gets splayer- pbody <- gets getPlayerBody- Config{configFirstDeathEnds} <- gets sconfig- when (bhp pbody <= 0) $ do- msgAdd $ actorVerb coactor pbody "die"- go <- displayMore ColorBW ""- recordHistory -- Prevent repeating the "die" msgs.- let bodyToCorpse = updateAnyActor pl $ \ body -> body {bsymbol = Just '%'}- animateDeath = do- diary <- getDiary- s <- get- let animFrs = animate s diary cops per $ deathBody (bloc pbody)- mapM_ displayFramePush $ animFrs- animateGameOver = do- animateDeath- bodyToCorpse- gameOver go- if configFirstDeathEnds- then animateGameOver- else case L.filter (/= pl) ahs of- [] -> animateGameOver- actor : _ -> do- msgAdd "The survivors carry on."- animateDeath- -- One last look at the beautiful world.- remember- -- Remove the dead player.- modify deletePlayer- -- At this place the invariant that the player exists fails.- -- Select the new player-controlled hero (invariant not needed),- -- but don't draw a frame for him with focusIfOurs,- -- in case the focus changes again during the same turn.- -- He's just a random next guy in the line.- selectPlayer actor- >>= assert `trueM` (pl, actor, "player resurrects")- -- At this place the invariant is restored again.---- | End game, showing the ending screens, if requested.-gameOver :: Bool -> Action ()-gameOver showEndingScreens = do- slid <- gets slid- modify (\ st -> st {squit = Just (False, Killed slid)})- when showEndingScreens $ do- Kind.COps{coitem=coitem@Kind.Ops{oname, ouniqGroup}} <- getCOps- s <- get- dng <- gets sdungeon- time <- gets stime- let (items, total) = calculateTotal coitem s- deepest = Dungeon.levelNumber slid -- use deepest visited instead of level of death- depth = Dungeon.depth dng- failMsg | timeFit time timeTurn < 300 =- "That song shall be short."- | total < 100 =- "Born poor, dies poor."- | deepest < 4 && total < 500 =- "This should end differently."- | deepest < depth - 1 =- "This defeat brings no dishonour."- | deepest < depth =- "That is your name. 'Almost'."- | otherwise =- "Dead heroes make better legends."- currencyName = MU.Text $ oname $ ouniqGroup "currency"- loseMsg = makePhrase- [ failMsg- , "You left"- , MU.NWs total currencyName- , "and some junk." ]- if null items- then modify (\ st -> st {squit = Just (True, Killed slid)})- else do- io <- itemOverlay True True items- tryIgnore $ do- displayOverAbort loseMsg io- modify (\ st -> st {squit = Just (True, Killed slid)})---- | Create a list of item names, split into many overlays.-itemOverlay ::Bool -> Bool -> [Item] -> Action [Overlay]-itemOverlay sorted cheat is = do- Kind.COps{coitem} <- getCOps- s <- get- lysize <- gets (lysize . slevel)- let items | sorted = L.sortBy (cmpLetterMaybe `on` jletter) is- | otherwise = is- pr i = makePhrase [ letterLabel (jletter i)- , MU.NWs (jcount i) $ partItemCheat cheat coitem s i ]- <> " "- return $ splitOverlay lysize $ L.map pr items--stopRunning :: Action ()-stopRunning = updatePlayerBody (\ p -> p { bdir = Nothing })---- | Perform look around in the current location of the cursor.-doLook :: ActionFrame ()-doLook = do- cops@Kind.COps{coactor} <- getCOps- loc <- gets (clocation . scursor)- state <- get- lvl <- gets slevel- hms <- gets (lactor . slevel)- per <- getPerception- target <- gets (btarget . getPlayerBody)- pl <- gets splayer- targeting <- gets (ctargeting . scursor)- assert (targeting /= TgtOff) $ do- let canSee = IS.member loc (totalVisible per)- ihabitant | canSee = L.find (\ m -> bloc m == loc) (IM.elems hms)- | otherwise = Nothing- monsterMsg = maybe "" (\ m -> actorVerb coactor m "be here") ihabitant- vis | not $ loc `IS.member` totalVisible per =- " (not visible)" -- by party- | actorReachesLoc pl loc per (Just pl) = ""- | otherwise = " (not reachable)" -- by hero- mode = case target of- TEnemy _ _ -> "[targeting monster" <> vis <> "]"- TLoc _ -> "[targeting location" <> vis <> "]"- TPath _ -> "[targeting path" <> vis <> "]"- TCursor -> "[targeting current" <> vis <> "]"- -- Show general info about current loc.- lookMsg = mode <+> lookAt cops True canSee state lvl loc monsterMsg- -- Check if there's something lying around at current loc.- is = lvl `rememberAtI` loc- io <- itemOverlay False False is- if length is > 2- then displayOverlays lookMsg "" io- else do- fr <- drawPrompt ColorFull lookMsg- returnFrame fr
− Game/LambdaHack/FOV.hs
@@ -1,68 +0,0 @@--- | Field Of View scanning with a variety of algorithms.--- See <https://github.com/kosmikus/LambdaHack/wiki/Fov-and-los>--- for discussion.-module Game.LambdaHack.FOV- ( fullscan- ) where--import qualified Data.List as L--import Game.LambdaHack.FOV.Common-import qualified Game.LambdaHack.FOV.Digital as Digital-import qualified Game.LambdaHack.FOV.Permissive as Permissive-import qualified Game.LambdaHack.FOV.Shadow as Shadow-import Game.LambdaHack.Point-import Game.LambdaHack.VectorXY-import Game.LambdaHack.Vector-import Game.LambdaHack.Level-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Content.TileKind-import qualified Game.LambdaHack.Tile as Tile-import Game.LambdaHack.Config---- | Perform a full scan for a given location. Returns the locations--- that are currently in the field of view. The Field of View--- algorithm to use, passed in the second argument, is set in the config file.-fullscan :: Kind.Ops TileKind -- ^ tile content, determines clear tiles- -> FovMode -- ^ scanning mode- -> Point -- ^ location of the spectacor- -> Level -- ^ the map that is scanned- -> [Point]-fullscan cotile fovMode loc Level{lxsize, lmap} =- case fovMode of- Shadow ->- L.concatMap (\ tr -> map tr (Shadow.scan (isCl . tr) 1 (0, 1))) tr8- Permissive ->- L.concatMap (\ tr -> map tr (Permissive.scan (isCl . tr))) tr4- Digital r ->- L.concatMap (\ tr -> map tr (Digital.scan r (isCl . tr))) tr4- Blind ->- let radiusOne = 1- in L.concatMap (\ tr -> map tr (Digital.scan radiusOne (isCl . tr))) tr4- where- isCl :: Point -> Bool- isCl = Tile.isClear cotile . (lmap Kind.!)-- trV xy = shift loc $ toVector lxsize $ VectorXY xy-- -- | 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)- ]-- -- | 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- ]
− Game/LambdaHack/FOV/Common.hs
@@ -1,67 +0,0 @@--- | Common definitions for the Field of View algorithms.--- See <https://github.com/kosmikus/LambdaHack/wiki/Fov-and-los>--- for some more context and references.-module Game.LambdaHack.FOV.Common- ( -- * Current scan parameters- Distance, Progress- -- * Scanning coordinate system- , Bump(..)- -- * Geometry in system @Bump@- , Line, ConvexHull, Edge, EdgeInterval- -- * Assorted minor operations- , maximal, steeper, addHull- ) where--import qualified Data.List as L--import Game.LambdaHack.PointXY---- | Distance from the (0, 0) point where FOV originates.-type Distance = Int--- | Progress along an arc with a constant distance from (0, 0).-type Progress = Int---- | Rotated and translated coordinates of 2D points, so that the points fit--- in a single quadrant area (e, g., quadrant I for Permissive FOV, hence both--- coordinates positive; adjacent diagonal halves of quadrant I and II--- for Digital FOV, hence y positive).--- 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)---- | Straight line between points.-type Line = (Bump, Bump)--- | Convex hull represented as a list of points.-type ConvexHull = [Bump]--- | An edge (comprising of a line and a convex hull)--- of the area to be scanned.-type Edge = (Line, ConvexHull)--- | The area left to be scanned, delimited by edges.-type EdgeInterval = (Edge, Edge)---- | 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)---- | 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)) =- (yf - y1)*(xf - x2) >= (yf - y2)*(xf - x1)---- | Extends a convex hull of bumps with a new bump. Nothing needs to be done--- if the new bump already lies within the hull. The first argument is--- typically `steeper`, optionally negated, applied to the second argument.-addHull :: (Bump -> Bump -> Bool) -- ^ a comparison function- -> Bump -- ^ a new bump to consider- -> ConvexHull -- ^ a convex hull of bumps represented as a list- -> ConvexHull-addHull gte new = (new :) . go- where- go (a:b:cs) | gte a b = go (b:cs)- go l = l
− Game/LambdaHack/FOV/Digital.hs
@@ -1,144 +0,0 @@--- | DFOV (Digital Field of View) implemented according to specification at <http://roguebasin.roguelikedevelopment.org/index.php?title=Digital_field_of_view_implementation>.--- This fast version of the algorithm, based on "PFOV", has AFAIK--- never been described nor implemented before.-module Game.LambdaHack.FOV.Digital- ( scan, dline, dsteeper, intersect, debugSteeper, debugLine- ) where--import Game.LambdaHack.Misc-import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.FOV.Common---- | Calculates the list of tiles, in @Bump@ coordinates, visible from (0, 0),--- within the given sight range.-scan :: Distance -- ^ visiblity radius- -> (Bump -> Bool) -- ^ clear tile predicate- -> [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)]))- where- dscan :: Distance -> EdgeInterval -> [Bump]- dscan d (s0@(sl{-shallow line-}, sBumps0), e@(el{-steep line-}, eBumps)) =- 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- -- Corners obstruct view, so the steep line, constructed- -- from corners, is itself not a part of the view,- -- 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]]- 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- 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-dline p1 p2 =- assert (uncurry blame $ debugLine (p1, p2)) $- (p1, p2)---- | Compare steepness of @(p1, f)@ and @(p2, f)@.--- Debug: Verify that the results of 2 independent checks are equal.-dsteeper :: Bump -> Bump -> Bump -> Bool-dsteeper f p1 p2 =- assert (res == debugSteeper f p1 p2) $- res- where res = steeper f p1 p2---- | The X coordinate, represented as a fraction, of the intersection of--- 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 =- assert (allB (>= 0) [y, yf]) $- ((d - y)*(xf - x) + x*(yf - y), yf - y)-{--Derivation of the formula:-The intersection point (xt, yt) satisfies the following equalities:-yt = d-(yt - y) (xf - x) = (xt - x) (yf - y)-hence-(yt - y) (xf - x) = (xt - x) (yf - y)-(d - y) (xf - x) = (xt - x) (yf - y)-(d - y) (xf - x) + x (yf - y) = xt (yf - y)-xt = ((d - y) (xf - x) + x (yf - y)) / (yf - y)--General remarks:-A diamond is denoted by its left corner. Hero at (0, 0).-Order of processing in the first quadrant rotated by 45 degrees is- 45678- 123- @-so the first processed diamond is at (-1, 1). The order is similar-as for the restrictive shadow casting algorithm and reversed wrt PFOV.-The line in the curent state of mscan is called the shallow line,-but it's the one that delimits the view from the left, while the steep-line is on the right, opposite to PFOV. We start scanning from the left.--The Point coordinates are cartesian. The Bump coordinates are cartesian,-translated so that the hero is at (0, 0) and rotated so that he always-looks at the first (rotated 45 degrees) quadrant. The (Progress, Distance)-cordinates coincide with the Bump coordinates, unlike in PFOV.--}---- | Debug functions for DFOV:---- | 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)) =- assert (allB (>= 0) [yf, y1, y2]) $- let (n1, k1) = intersect (p1, f) 0- (n2, k2) = intersect (p2, f) 0- in n1 * k2 >= k1 * n2---- | Debug: check is a view border line for DFOV is legal.-debugLine :: Line -> (Bool, String)-debugLine line@(B(x1, y1), B(x2, y2))- | not (allB (>= 0) [y1, y2]) =- (False, "negative coordinates: " ++ show line)- | y1 == y2 && x1 == x2 =- (False, "ill-defined line: " ++ show line)- | y1 == y2 =- (False, "horizontal line: " ++ show line)- | crossL0 =- (False, "crosses the X axis below 0: " ++ show line)- | crossG1 =- (False, "crosses the X axis above 1: " ++ show line)- | otherwise = (True, "")- where- (n, k) = intersect line 0- (q, r) = if k == 0 then (0, 0) else n `divMod` k- crossL0 = q < 0 -- q truncated toward negative infinity- crossG1 = q >= 1 && (q > 1 || r /= 0)
− Game/LambdaHack/FOV/Permissive.hs
@@ -1,154 +0,0 @@--- | PFOV (Permissive Field of View) clean-room reimplemented based on the algorithm described in <http://roguebasin.roguelikedevelopment.org/index.php?title=Precise_Permissive_Field_of_View>,--- though the general structure is more influenced by recursive shadow casting,--- as implemented in Shadow.hs. In the result, this algorithm is much faster--- than the original algorithm on dense maps, since it does not scan--- areas blocked by shadows.-module Game.LambdaHack.FOV.Permissive- ( scan, dline, dsteeper, intersect, debugSteeper, debugLine- ) where--import Game.LambdaHack.Misc-import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.FOV.Common---- TODO: Scanning squares on horizontal lines in octants, not squares--- on diagonals in quadrants, may be much faster and a bit simpler.--- Right now we build new view on each end of each visible wall tile--- and this is necessary only for straight, thin, diagonal walls.---- | Calculates the list of tiles, in @Bump@ coordinates, visible from (0, 0).-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)]))- where- dscan :: Distance -> EdgeInterval -> [Bump]- dscan d (s0@(sl{-shallow line-}, sBumps0), e@(el{-steep line-}, eBumps)) =- assert (d >= 0 && pe + 1 >= ps0 && ps0 >= 0- `blame` (d,s0,e,ps0,pe)) $- if illegal then [] else inside ++ outside- where- (ns, ks) = intersect sl d- (ne, ke) = intersect el d- -- Corners are translucent, so they are invisible, so if intersection- -- is at a corner, choose pe that creates the smaller view.- (ps0, pe) = (ns `div` ks, ne `divUp` ke - 1) -- progress interval to check- -- 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)-- 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-- -- 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- | 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-- mscan Nothing 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- -- 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---- | 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)---- | Compare steepness of @(p1, f)@ and @(p2, f)@.--- Debug: Verify that the results of 2 independent checks are equal.-dsteeper :: Bump -> Bump -> Bump -> Bool-dsteeper f p1 p2 =- assert (res == debugSteeper f p1 p2) $- res- where res = steeper f p1 p2---- | The Y coordinate, represented as a fraction, of the intersection of--- 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 =- assert (allB (>= 0) [x, y, xf, yf]) $- ((1 + d)*(yf - y) + y*xf - x*yf, (xf - x) + (yf - y))-{--Derivation of the formula:-The intersection point (xt, yt) satisfies the following equalities:-xt = 1 + d - yt-(yt - y) (xf - x) = (xt - x) (yf - y)-hence-(yt - y) (xf - x) = (xt - x) (yf - y)-yt (xf - x) - y xf = xt (yf - y) - x yf-yt (xf - x) - y xf = (1 + d) (yf - y) - yt (yf - y) - x yf-yt (xf - x) + yt (yf - y) = (1 + d) (yf - y) - x yf + y xf-yt = ((1 + d) (yf - y) + y xf - x yf) / (xf - x + yf - y)--General remarks:-A square is denoted by its bottom-left corner. Hero at (0, 0).-Order of processing in the first quadrant is-9-58-247-@136-so the first processed square is at (0, 1). The order is reversed-wrt the restrictive shadow casting algorithm. The line in the curent state-of mscan is not the steep line, but the shallow line,-and we start scanning from the bottom right.--The Point coordinates are cartesian. The Bump coordinates are cartesian,-translated so that the hero is at (0, 0) and rotated so that he always-looks at the first quadrant. The (Progress, Distance) cordinates-are mangled and not used for geometry.--}---- | Debug functions for PFOV:---- | 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)) =- assert (allB (>= 0) [xf, yf, x1, y1, x2, y2]) $- let (n1, k1) = intersect (p1, f) 0- (n2, k2) = intersect (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))- | not (allB (>= 0) [x1, y1, x2, y2]) =- (False, "negative coordinates: " ++ show line)- | y1 == y2 && x1 == x2 =- (False, "ill-defined line: " ++ show line)- | x2 - x1 == - (y2 - y1) =- (False, "diagonal line: " ++ show line)- | crossL0 =- (False, "crosses diagonal below 0: " ++ show line)- | crossG1 =- (False, "crosses diagonal above 1: " ++ show line)- | otherwise = (True, "")- where- (n, k) = intersect line 0- (q, r) = if k == 0 then (0, 0) else n `divMod` k- crossL0 = q < 0 -- q truncated toward negative infinity- crossG1 = q >= 1 && (q > 1 || r /= 0)
− Game/LambdaHack/FOV/Shadow.hs
@@ -1,111 +0,0 @@--- | A restrictive variant of Recursive Shadow Casting FOV with infinite range.--- It's not designed for dungeons with diagonal walls and so here--- they block visibility, though they don't block movement.--- The main advantage of the algorithm is that it's very simple and fast.-module Game.LambdaHack.FOV.Shadow (SBump, Interval, scan) where--import Data.Ratio--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.FOV.Common--{--Field Of View----------------The algorithm used is a variant of Shadow Casting. We first compute-fields that are reachable (have unobstructed line of sight) from the hero's-position. Later, in Perception.hs, from this information we compute-the fields that are visible (not hidden in darkness, etc.).--As input to the algorithm, we require information about fields that-block light. As output, we get information on the reachability of all fields.-We assume that the hero is located at position (0, 0)-and we only consider fields (line, row) where line >= 0 and 0 <= row <= line.-This is just about one eighth of the whole hero's surroundings,-but the other parts can be computed in the same fashion by mirroring-or rotating the given algorithm accordingly.-- fov (blocks, maxline) =- shadow := \empty_set- reachable (0, 0) := True- for l \in [ 1 .. maxline ] do- for r \in [ 0 .. l ] do- reachable (l, r) := ( \exists a. a \in interval (l, r) \and- a \not_in shadow)- if blocks (l, r) then- shadow := shadow \union interval (l, r)- end if- end for- end for- return reachable-- interval (l, r) = return [ angle (l + 0.5, r - 0.5),- angle (l - 0.5, r + 0.5) ]- angle (l, r) = return atan (r / l)--The algorithm traverses the fields line by line, row by row.-At every moment, we keep in shadow the intervals which are in shadow,-measured by their angle. A square is reachable when any point-in it is not in shadow --- the algorithm is permissive in this respect.-We could also require that a certain fraction of the field is reachable,-or a specific point. Our choice has certain consequences. For instance,-a single blocking field throws a shadow, but the fields immediately behind-the blocking field are still visible.--We can compute the interval of angles corresponding to one square field-by computing the angle of the line passing the upper left corner-and the angle of the line passing the lower right corner.-This is what interval and angle do. If a field is blocking, the interval-for the square is added to the shadow set.--}---- | Rotated and translated coordinates of 2D points, so that they fit--- in the same single octant area.-type SBump = (Progress, Distance)---- | The area left to be scanned, delimited by fractions of the original arc.--- Interval @(0, 1)@ means the whole 45 degrees arc of the processed octant--- is to be scanned.-type Interval = (Rational, Rational)---- TODO: if ever used, apply static argument transformation to isClear.--- | Calculates the list of tiles, in @SBump@ coordinates, visible from (0, 0).-scan :: (SBump -> Bool) -- ^ clear tile predicate- -> Distance -- ^ the current distance from (0, 0)- -> Interval -- ^ the current interval to scan- -> [SBump]-scan isClear d (s0, e) =- let ps = downBias (s0 * fromIntegral d) -- minimal progress to consider- pe = upBias (e * fromIntegral d) -- maximal progress to consider- inside = [(p, d) | p <- [ps..pe]]- outside- | isClear (ps, d) = mscan (Just s0) ps pe -- start in light- | otherwise = mscan Nothing ps pe -- start in shadow- in assert (d >= 0 && e >= 0 && s0 >= 0 && pe >= ps && ps >= 0- `blame` (d,s0,e,ps,pe)) $- inside ++ outside- where- -- The current state of a scan is kept in @Maybe Rational@.- -- If it's the @Just@ case, we're in a visible interval. If @Nothing@,- -- we're in a shadowed interval.- mscan :: Maybe Rational -> Progress -> Progress -> [SBump]- mscan (Just s) ps pe- | s >= e = [] -- empty interval- | ps > pe = scan isClear (d+1) (s, e) -- reached end, scan next- | not $ isClear (ps, d) = -- entering shadow- let ne = (fromIntegral ps - (1%2)) / (fromIntegral d + (1%2))- in mscan Nothing (ps+1) pe ++ scan isClear (d+1) (s, ne)- | otherwise = mscan (Just s) (ps+1) pe -- continue in light-- mscan Nothing ps pe- | ps > pe = [] -- reached end while in shadow- | isClear (ps, d) = -- moving out of shadow- let ns = (fromIntegral ps - (1%2)) / (fromIntegral d - (1%2))- in mscan (Just ns) (ps+1) pe- | otherwise = mscan Nothing (ps+1) pe -- continue in shadow---downBias, upBias :: (Integral a, Integral b) => Ratio a -> b-downBias x = round (x - 1 % (denominator x * 3))-upBias x = round (x + 1 % (denominator x * 3))
− Game/LambdaHack/Feature.hs
@@ -1,32 +0,0 @@--- | Terrain tile features.-module Game.LambdaHack.Feature- ( Feature(..)- ) where--import Data.Text (Text)--import Game.LambdaHack.Effect-import Game.LambdaHack.Random---- | All possible terrain tile features, some of them parameterized or dependent--- on outside coefficients, e.g., on the tile secrecy value.-data Feature =- Ascendable -- ^ triggered by ascending- | Descendable -- ^ triggered by descending- | Openable -- ^ triggered by opening- | Closable -- ^ triggered by closing- | Hidden -- ^ triggered when the tile's secrecy becomes zero-- | Cause !Effect -- ^ causes the effect when triggered- | ChangeTo !Text -- ^ transitions to any tile of the group when triggered-- | Walkable -- ^ actors can walk through- | Clear -- ^ actors can see through- | Lit -- ^ is lit with an ambient shine- | Aura !Effect -- ^ sustains the effect continuously, TODO-- | Boring -- ^ items and stairs can be generated there- | Exit -- ^ is a (not hidden) door, stair, etc.- | Path -- ^ used for visible paths throughout the level- | Secret !RollDice -- ^ discovering the secret will require this many turns- deriving (Show, Read, Eq, Ord)
− Game/LambdaHack/Flavour.hs
@@ -1,95 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | The appearance of in-game items, as communicated to the player.-module Game.LambdaHack.Flavour- ( -- * The @Flavour@ type- Flavour- , -- * Constructors- zipPlain, zipFancy, darkCol, brightCol, stdCol, stdFlav- , -- * Accessors- flavourToColor, flavourToName- ) where--import qualified Data.List as L-import Data.Binary-import Data.Text (Text)--import Game.LambdaHack.Color---- TODO: add more variety, as the number of items increases--- | The type of item flavours.-data Flavour = Flavour- { fancyName :: Bool -- ^ should the colour description be fancy or plain- , baseColor :: Color -- ^ the colour of the flavour- }- deriving (Show, Eq, Ord)--instance Binary Flavour where- put Flavour{..} = do- put fancyName- put baseColor- get = do- fancyName <- get- baseColor <- get- return Flavour{..}---- | Turn a colour set into a flavour set.-zipPlain, zipFancy :: [Color] -> [Flavour]-zipPlain = L.map (Flavour False)-zipFancy = L.map (Flavour True)---- | Colour sets.-darkCol, brightCol, stdCol :: [Color]-darkCol = [Red .. Cyan]-brightCol = [BrRed .. BrCyan] -- BrBlack is not really that bright-stdCol = darkCol ++ brightCol---- | The standard full set of flavours.-stdFlav :: [Flavour]-stdFlav = zipPlain stdCol ++ zipFancy stdCol---- | Get the underlying base colour of a flavour.-flavourToColor :: Flavour -> Color-flavourToColor Flavour{baseColor} = baseColor---- | Construct the full name of a flavour.-flavourToName :: Flavour -> Text-flavourToName Flavour{..} | fancyName = colorToFancyName baseColor-flavourToName Flavour{..} = colorToPlainName baseColor---- | Human-readable names, for item colors. The simple set.-colorToPlainName :: Color -> Text-colorToPlainName Black = "black"-colorToPlainName Red = "red"-colorToPlainName Green = "green"-colorToPlainName Brown = "brown"-colorToPlainName Blue = "blue"-colorToPlainName Magenta = "purple"-colorToPlainName Cyan = "cyan"-colorToPlainName White = "ivory"-colorToPlainName BrBlack = "gray"-colorToPlainName BrRed = "coral"-colorToPlainName BrGreen = "lime"-colorToPlainName BrYellow = "yellow"-colorToPlainName BrBlue = "azure"-colorToPlainName BrMagenta = "pink"-colorToPlainName BrCyan = "aquamarine"-colorToPlainName BrWhite = "white"---- | Human-readable names, for item colors. The fancy set.-colorToFancyName :: Color -> Text-colorToFancyName Black = "smoky black"-colorToFancyName Red = "apple red"-colorToFancyName Green = "forest green"-colorToFancyName Brown = "mahogany"-colorToFancyName Blue = "royal blue"-colorToFancyName Magenta = "indigo"-colorToFancyName Cyan = "teal"-colorToFancyName White = "silver gray"-colorToFancyName BrBlack = "charcoal"-colorToFancyName BrRed = "salmon"-colorToFancyName BrGreen = "emerald"-colorToFancyName BrYellow = "amber"-colorToFancyName BrBlue = "sky blue"-colorToFancyName BrMagenta = "magenta"-colorToFancyName BrCyan = "turquoise"-colorToFancyName BrWhite = "ghost white"
+ Game/LambdaHack/Frontend.hs view
@@ -0,0 +1,207 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Display game data on the screen and receive user input+-- using one of the available raw frontends and derived operations.+module Game.LambdaHack.Frontend+ ( -- * Re-exported part of the raw frontend+ frontendName+ -- * Derived operation+ , startupF, getConfirmGeneric+ -- * Connection channels+ , ChanFrontend, FrontReq(..), ConnMulti(..), connMulti, loopFrontend+ ) where++import Control.Concurrent+import Control.Concurrent.STM (TQueue, atomically, newTQueueIO)+import qualified Control.Concurrent.STM as STM+import Control.Monad+import qualified Data.EnumMap.Strict as EM+import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T+import System.IO.Unsafe (unsafePerformIO)++import Game.LambdaHack.Common.Animation+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Key as K+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Frontend.Chosen+import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.LQueue++type ChanFrontend = TQueue K.KM++type FromMulti = MVar (Int, FactionId -> (ChanFrontend, Text))++type ToMulti = TQueue (FactionId, FrontReq)++data FrontReq =+ FrontFrame {frontAc :: !AcFrame}+ -- ^ show a frame, if the fid acitve, or save it to the client's queue+ | FrontKey {frontKM :: ![K.KM], frontFr :: !SingleFrame}+ -- ^ flush frames, possibly show fadeout/fadein and ask for a keypress+ | FrontSlides {frontClear :: [K.KM], frontSlides :: [SingleFrame]}+ -- ^ show a whole slideshow without interleaving with other clients++type ReqMap = EM.EnumMap FactionId (LQueue AcFrame)++-- | Multiplex connection channels, for the case of a frontend shared+-- among clients. This is transparent to the clients themselves.+data ConnMulti = ConnMulti+ { fromMulti :: FromMulti+ , toMulti :: ToMulti+ }++startupF :: String -> IO () -> IO ()+startupF s k = startup s $ \fs -> do+ void $ forkIO $ loopFrontend fs connMulti+ k++-- | Display a prompt, wait for any of the specified keys (for any key,+-- if the list is empty). Repeat if an unexpected key received.+promptGetKey :: FrontendSession -> [K.KM] -> SingleFrame -> IO K.KM+promptGetKey sess keys frame = do+ km <- promptGetAnyKey sess frame+ if null keys || km `elem` keys+ then return km+ else promptGetKey sess keys frame++-- TODO: avoid unsafePerformIO; but server state is a wrong idea, too+connMulti :: ConnMulti+{-# NOINLINE connMulti #-}+connMulti = unsafePerformIO $ do+ fromMulti <- newMVar undefined+ toMulti <- newTQueueIO+ return ConnMulti{..}++-- | Augment a function that takes and returns keys.+getConfirmGeneric :: Monad m+ => ([K.KM] -> a -> m K.KM)+ -> [K.KM] -> a -> m Bool+getConfirmGeneric pGetKey clearKeys x = do+ let extraKeys = [ K.KM {key=K.Space, modifier=K.NoModifier}+ , K.KM {key=K.Esc, modifier=K.NoModifier} ]+ km <- pGetKey (clearKeys ++ extraKeys) x+ return $! km /= K.KM {key=K.Esc, modifier=K.NoModifier}++flushFrames :: FrontendSession -> FactionId -> ReqMap -> IO ReqMap+flushFrames fs fid reqMap = do+ let queue = toListLQueue $ fromMaybe newLQueue $ EM.lookup fid reqMap+ reqMap2 = EM.delete fid reqMap+ mapM_ (displayAc fs) queue+ return reqMap2++displayAc :: FrontendSession -> AcFrame -> IO ()+displayAc fs (AcConfirm fr) = void $ getConfirmGeneric (promptGetKey fs) [] fr+displayAc fs (AcRunning fr) = display fs True (Just fr)+displayAc fs (AcNormal fr) = display fs False (Just fr)+displayAc fs AcDelay = display fs False Nothing++getSingleFrame :: AcFrame -> Maybe SingleFrame+getSingleFrame (AcConfirm fr) = Just fr+getSingleFrame (AcRunning fr) = Just fr+getSingleFrame (AcNormal fr) = Just fr+getSingleFrame AcDelay = Nothing++toSingles :: FactionId -> ReqMap -> [SingleFrame]+toSingles fid reqMap =+ let queue = toListLQueue $ fromMaybe newLQueue $ EM.lookup fid reqMap+ in mapMaybe getSingleFrame queue++fadeF :: FrontendSession -> Bool -> FactionId -> Text -> SingleFrame -> IO ()+fadeF fs out side pname frame = do+ let topRight = True+ lxsize = xsizeSingleFrame frame+ lysize = ysizeSingleFrame frame+ msg = "Player" <+> showT (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}+ animFrs = renderAnim lxsize lysize basicFrame animMap+ frs | out = animFrs+ -- Empty frame to mark the fade-in end,+ -- to trim only to here if SPACE pressed.+ | otherwise = animFrs ++ [Nothing]+ mapM_ (display fs False) frs++insertFr :: FactionId -> AcFrame -> ReqMap -> ReqMap+insertFr fid fr reqMap =+ let queue = fromMaybe newLQueue $ EM.lookup fid reqMap+ in EM.insert fid (writeLQueue queue fr) reqMap++-- Read UI requests from clients and send them to the frontend,+-- separated by fadeout/fadein frame sequences, if needed.+-- There may be many UI clients, but this function is only ever+-- executed on one thread, so the frontend receives requests+-- in a sequential way, without any random interleaving.+loopFrontend :: FrontendSession -> ConnMulti -> IO ()+loopFrontend fs ConnMulti{..} = loop Nothing EM.empty+ where+ writeKM :: FactionId -> K.KM -> IO ()+ writeKM fid km = do+ fM <- takeMVar fromMulti+ let chanFrontend = fst $ snd fM fid+ atomically $ STM.writeTQueue chanFrontend km+ putMVar fromMulti fM++ flushFade :: SingleFrame -> Maybe (FactionId, SingleFrame)+ -> ReqMap -> FactionId+ -> IO ReqMap+ flushFade frontFr oldFidFrame reqMap fid =+ if Just fid == fmap fst oldFidFrame then+ return reqMap+ else do+ (nH, fCT) <- readMVar fromMulti+ let pname = snd $ fCT fid+ reqMap2 <- case oldFidFrame of+ Nothing -> return reqMap+ Just (oldFid, oldFrame) -> do+ reqMap2 <- flushFrames fs oldFid reqMap+ let singles = toSingles oldFid reqMap -- not @reqMap2@!+ lastFrame = fromMaybe oldFrame $ listToMaybe $ reverse singles+ fadeF fs True fid pname lastFrame+ return reqMap2+ let singles = toSingles fid reqMap2+ firstFrame = fromMaybe frontFr $ listToMaybe singles+ -- @nH@ is unreliable, except at the game start.+ unless (isNothing oldFidFrame && nH < 2) $+ fadeF fs False fid pname firstFrame+ flushFrames fs fid reqMap2++ loop :: Maybe (FactionId, SingleFrame) -> ReqMap -> IO ()+ loop oldFidFrame reqMap = do+ (fid, efr) <- atomically $ STM.readTQueue toMulti+ case efr of+ FrontFrame{..} | Just (oldFid, oldFrame) <- oldFidFrame+ , fid == oldFid -> do+ displayAc fs frontAc+ let frame = fromMaybe oldFrame $ getSingleFrame frontAc+ loop (Just (fid, frame)) reqMap+ FrontFrame{..} -> do+ let reqMap2 = insertFr fid frontAc reqMap+ loop oldFidFrame reqMap2+ FrontKey{..} -> do+ reqMap2 <- flushFade frontFr oldFidFrame reqMap fid+ km <- promptGetKey fs frontKM frontFr+ writeKM fid km+ loop (Just (fid, frontFr)) reqMap2+ FrontSlides{frontSlides = []} -> return ()+ FrontSlides{frontSlides = frontSlides@(fr1 : _), ..} -> do+ reqMap2 <- flushFade fr1 oldFidFrame reqMap fid+ let go frs =+ case frs of+ [] -> assert `failure` fid+ [x] -> do+ display fs False (Just x)+ writeKM fid K.KM {key=K.Space, modifier=K.NoModifier}+ return x+ x : xs -> do+ b <- getConfirmGeneric (promptGetKey fs) frontClear x+ if b then go xs+ else do+ writeKM fid K.KM {key=K.Esc, modifier=K.NoModifier}+ return x+ frLast <- go frontSlides+ loop (Just (fid, frLast)) reqMap2
+ Game/LambdaHack/Frontend/Chosen.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE CPP #-}+-- | Re-export the operations of the chosen raw frontend+-- (determined at compile time with cabal flags).+module Game.LambdaHack.Frontend.Chosen+ ( -- * Re-exported raw frontend+ FrontendSession, startup, frontendName, promptGetAnyKey, display+ ) where++-- Wrapper for selected Display frontend.++#ifdef GTK+import Game.LambdaHack.Frontend.Gtk as D+#elif VTY+import Game.LambdaHack.Frontend.Vty as D+#elif CURSES+import Game.LambdaHack.Frontend.Curses as D+#elif STD+import Game.LambdaHack.Frontend.Std as D+#else+import Game.LambdaHack.Frontend.Gtk as D+#endif
+ Game/LambdaHack/Frontend/Curses.hs view
@@ -0,0 +1,158 @@+-- | Text frontend based on HSCurses.+module Game.LambdaHack.Frontend.Curses+ ( -- * Session data type for the frontend+ FrontendSession+ -- * The output and input operations+ , display, promptGetAnyKey+ -- * Frontend administration tools+ , frontendName, startup+ ) where++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 Game.LambdaHack.Common.Animation (SingleFrame (..))+import qualified Game.LambdaHack.Common.Color as Color+import qualified Game.LambdaHack.Common.Key as K (KM (..), Key (..),+ Modifier (..))+import Game.LambdaHack.Utils.Assert++-- | Session data maintained by the frontend.+data FrontendSession = FrontendSession+ { swin :: C.Window -- ^ the window to draw to+ , sstyles :: M.Map Color.Attr C.CursesStyle+ -- ^ map from fore/back colour pairs to defined curses styles+ }++-- | The name of the frontend.+frontendName :: String+frontendName = "curses"++-- | Starts the main program loop using the frontend input and output.+startup :: String -> (FrontendSession -> IO ()) -> IO ()+startup _ k = do+ C.start+-- C.keypad C.stdScr False -- TODO: may help to fix xterm keypad on Ubuntu+ void $ C.cursSet C.CursorInvisible+ let s = [ (Color.Attr{fg, bg}, C.Style (toFColor fg) (toBColor bg))+ | fg <- [minBound..maxBound],+ -- No more color combinations possible: 16*4, 64 is max.+ bg <- Color.legalBG ]+ nr <- C.colorPairs+ when (nr < L.length s) $+ C.end >>+ (assert `failure`+ "Terminal has too few color pairs (" ++ show nr ++ "). Giving up.")+ let (ks, vs) = unzip s+ ws <- C.convertStyles vs+ let styleMap = M.fromList (zip ks ws)+ k (FrontendSession C.stdScr styleMap)+ C.end++-- | Output to the screen via the frontend.+display :: FrontendSession -- ^ frontend session data+ -> Bool+ -> Maybe SingleFrame -- ^ the screen frame to draw+ -> IO ()+display _ _ Nothing = return ()+display FrontendSession{..} _ (Just SingleFrame{..}) = do+ -- 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+ sequence_ [ C.setStyle (M.findWithDefault defaultStyle acAttr sstyles)+ >> C.mvWAddStr swin (y + 1) x [acChar]+ | (y, line) <- nm, (x, Color.AttrChar{..}) <- line ]+ C.refresh++-- | Input key via the frontend.+nextEvent :: FrontendSession -> Maybe Bool -> IO K.KM+nextEvent _sess _ = keyTranslate `fmap` C.getKey C.refresh++-- | Display a prompt, wait for any key.+promptGetAnyKey :: FrontendSession -> SingleFrame+ -> IO K.KM+promptGetAnyKey sess frame = do+ display sess True $ Just frame+ nextEvent sess Nothing++keyTranslate :: C.Key -> K.KM+keyTranslate e = (\(key, modifier) -> K.KM {..}) $+ case e of+ C.KeyChar '\ESC' -> (K.Esc, K.NoModifier)+ C.KeyExit -> (K.Esc, K.NoModifier)+ C.KeyChar '\n' -> (K.Return, K.NoModifier)+ C.KeyChar '\r' -> (K.Return, K.NoModifier)+ C.KeyEnter -> (K.Return, K.NoModifier)+ C.KeyChar ' ' -> (K.Space, K.NoModifier)+ C.KeyChar '\t' -> (K.Tab, K.NoModifier)+ C.KeyBTab -> (K.BackTab, K.NoModifier)+ C.KeyUp -> (K.Up, K.NoModifier)+ C.KeyDown -> (K.Down, K.NoModifier)+ C.KeyLeft -> (K.Left, K.NoModifier)+ C.KeySLeft -> (K.Left, K.NoModifier)+ 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.KeyNPage -> (K.PgDn, K.NoModifier)+ C.KeyBeg -> (K.Begin, K.NoModifier)+ C.KeyB2 -> (K.Begin, K.NoModifier)+ C.KeyClear -> (K.Begin, K.NoModifier)+ -- No KP_ keys; see <https://github.com/skogsbaer/hscurses/issues/10>+ -- TODO: try to get the Control modifier for keypad keys from the escape+ -- gibberish and use Control-keypad for KP_ movement.+ C.KeyChar c+ -- This case needs to be considered after Tab, since, apparently,+ -- on some terminals ^i == Tab and Tab is more important for us.+ | 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,+ -- 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 (show e), K.NoModifier)++toFColor :: Color.Color -> C.ForegroundColor+toFColor Color.Black = C.BlackF+toFColor Color.Red = C.DarkRedF+toFColor Color.Green = C.DarkGreenF+toFColor Color.Brown = C.BrownF+toFColor Color.Blue = C.DarkBlueF+toFColor Color.Magenta = C.PurpleF+toFColor Color.Cyan = C.DarkCyanF+toFColor Color.White = C.WhiteF+toFColor Color.BrBlack = C.GreyF+toFColor Color.BrRed = C.RedF+toFColor Color.BrGreen = C.GreenF+toFColor Color.BrYellow = C.YellowF+toFColor Color.BrBlue = C.BlueF+toFColor Color.BrMagenta = C.MagentaF+toFColor Color.BrCyan = C.CyanF+toFColor Color.BrWhite = C.BrightWhiteF++toBColor :: Color.Color -> C.BackgroundColor+toBColor Color.Black = C.BlackB+toBColor Color.Red = C.DarkRedB+toBColor Color.Green = C.DarkGreenB+toBColor Color.Brown = C.BrownB+toBColor Color.Blue = C.DarkBlueB+toBColor Color.Magenta = C.PurpleB+toBColor Color.Cyan = C.DarkCyanB+toBColor Color.White = C.WhiteB+toBColor _ = C.BlackB -- a limitation of curses
+ Game/LambdaHack/Frontend/Gtk.hs view
@@ -0,0 +1,417 @@+-- | Text frontend based on Gtk.+{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}+module Game.LambdaHack.Frontend.Gtk+ ( -- * Session data type for the frontend+ FrontendSession+ -- * The output and input operations+ , display, promptGetAnyKey+ -- * Frontend administration tools+ , frontendName, startup+ ) where++import Control.Concurrent+import Control.Monad+import Control.Monad.Reader+import qualified Data.ByteString.Char8 as BS+import Data.IORef+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 Game.LambdaHack.Common.Animation (SingleFrame (..))+import qualified Game.LambdaHack.Common.Color as Color+import qualified Game.LambdaHack.Common.Key as K (KM (..), Modifier (..),+ keyTranslate)+import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.LQueue++data FrameState =+ FPushed -- frames stored in a queue, to be drawn in equal time intervals+ { fpushed :: !(LQueue (Maybe GtkFrame)) -- ^ screen output channel+ , fshown :: !GtkFrame -- ^ last full frame shown+ }+ | FNone -- no frames stored++-- | Session data maintained by the frontend.+data FrontendSession = FrontendSession+ { sview :: !TextView -- ^ the widget to draw to+ , stags :: !(M.Map Color.Attr TextTag) -- ^ text color tags for fg/bg+ , schanKey :: !(Chan K.KM) -- ^ channel for keyboard input+ , sframeState :: !(MVar FrameState)+ -- ^ state of the frame finite machine; this mvar is locked+ -- for a short time only, because it's needed, among others,+ -- to display frames, which is done by a single polling thread,+ -- in real time+ , slastFull :: !(MVar (GtkFrame, Bool))+ -- ^ most recent full (not empty, not repeated) frame received+ -- and if any empty frame followed it; this mvar is locked+ -- for longer intervals to ensure that threads (possibly many)+ -- add frames in an orderly manner, which is not done in real time,+ -- though sometimes the frame display subsystem has to poll+ -- for a frame, in which case the locking interval becomes meaningful+ }++data GtkFrame = GtkFrame+ { gfChar :: !BS.ByteString+ , gfAttr :: ![[TextTag]]+ }+ deriving Eq++dummyFrame :: GtkFrame+dummyFrame = GtkFrame BS.empty []++-- | Perform an operation on the frame queue.+onQueue :: (LQueue (Maybe GtkFrame) -> LQueue (Maybe GtkFrame))+ -> FrontendSession -> IO ()+onQueue f FrontendSession{sframeState} = do+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} ->+ putMVar sframeState FPushed{fpushed = f fpushed, ..}+ _ ->+ 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"++-- | Starts GTK. The other threads have to be spawned+-- after gtk is initialized, because they call @postGUIAsync@,+-- and need @sview@ and @stags@. Because of Windows, GTK needs to be+-- on a bound thread, so we can't avoid the communication overhead+-- of bound threads, so there's no point spawning a separate thread for GTK.+startup :: String -> (FrontendSession -> IO ()) -> IO ()+startup = runGtk++-- | Sets up and starts the main GTK loop providing input and output.+runGtk :: String -> (FrontendSession -> IO ()) -> IO ()+runGtk configFont k = do+ -- Init GUI.+ unsafeInitGUIForThreadedRTS+ -- Text attributes.+ ttt <- textTagTableNew+ stags <- fmap M.fromList $+ mapM (\ ak -> do+ tt <- textTagNew Nothing+ textTagTableAdd ttt tt+ doAttr tt ak+ return (ak, tt))+ [ Color.Attr{fg, bg}+ | fg <- [minBound..maxBound], bg <- Color.legalBG ]+ -- Text buffer.+ tb <- textBufferNew (Just ttt)+ -- Create text view. TODO: use GtkLayout or DrawingArea instead of TextView?+ sview <- textViewNewWithBuffer tb+ textViewSetEditable sview False+ textViewSetCursorVisible sview False+ -- Set up the channel for keyboard input.+ schanKey <- newChan+ -- Set up the frame state.+ let frameState = FNone+ -- Create the session record.+ sframeState <- newMVar frameState+ slastFull <- newMVar (dummyFrame, False)+ let sess = FrontendSession{..}+ -- Fork the game logic thread. When logic ends, game exits.+ -- TODO: is postGUIAsync needed here?+ forkIO $ k sess >> postGUIAsync mainQuit+ -- Fork the thread that periodically draws a frame from a queue, if any.+ forkIO $ pollFrames sess Nothing+ -- Fill the keyboard channel.+ sview `on` keyPressEvent $ do+ n <- eventKeyName+ mods <- eventModifier+ 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}+ return True+ -- Set the font specified in config, if any.+ f <- fontDescriptionFromString configFont+ widgetModifyFont sview (Just f)+ -- Prepare font chooser dialog.+ currentfont <- newIORef f+ sview `on` buttonPressEvent $ do+ but <- eventButton+ liftIO $ case but of+ RightButton -> do+ fsd <- fontSelectionDialogNew "Choose font"+ cf <- readIORef currentfont -- TODO: "Terminus,Monospace" fails+ fds <- fontDescriptionToString cf+ fontSelectionDialogSetFontName fsd fds+ fontSelectionDialogSetPreviewText fsd "eee...@.##+##"+ resp <- dialogRun fsd+ when (resp == ResponseOk) $ do+ fn <- fontSelectionDialogGetFontName fsd+ case fn of+ Just fn' -> do+ fd <- fontDescriptionFromString fn'+ writeIORef currentfont fd+ widgetModifyFont sview (Just fd)+ Nothing -> return ()+ widgetDestroy fsd+ return True+ _ -> return False+ -- Modify default colours.+ let black = Color minBound minBound minBound -- Color.defBG == Color.Black+ white = Color 0xC500 0xBC00 0xB800 -- Color.defFG == Color.White+ widgetModifyBase sview StateNormal black+ widgetModifyText sview StateNormal white+ -- Set up the main window.+ w <- windowNew+ containerAdd w sview+ onDestroy w mainQuit+ widgetShowAll w+ -- Wait until the other thread draws something and show the window.+ yield+ mainGUI++-- | Output to the screen via the frontend.+output :: FrontendSession -- ^ frontend session data+ -> GtkFrame -- ^ the screen frame to draw+ -> IO ()+output FrontendSession{sview, stags} GtkFrame{..} = do -- new frame+ tb <- textViewGetBuffer sview+ let attrs = zip [0..] gfAttr+ defAttr = stags M.! Color.defAttr+ textBufferSetByteString tb gfChar+ mapM_ (setTo tb defAttr 0) attrs++setTo :: TextBuffer -> TextTag -> Int -> (Int, [TextTag]) -> IO ()+setTo _ _ _ (_, []) = return ()+setTo tb defAttr lx (ly, attr:attrs) = do+ ib <- textBufferGetIterAtLineOffset tb (ly + 1) lx+ ie <- textIterCopy ib+ let setIter :: TextTag -> Int -> [TextTag] -> IO ()+ setIter previous repetitions [] = do+ textIterForwardChars ie repetitions+ when (previous /= defAttr) $+ textBufferApplyTag tb previous ib ie+ setIter previous repetitions (a:as)+ | a == previous =+ setIter a (repetitions + 1) as+ | otherwise = do+ textIterForwardChars ie repetitions+ when (previous /= defAttr) $+ textBufferApplyTag tb previous ib ie+ textIterForwardChars ib repetitions+ setIter a 1 as+ setIter attr 1 attrs++-- TODO: configure+-- | Maximal frames per second.+-- This is better low and fixed, to avoid jerkiness and delays+-- that tell the player there are many intelligent enemies on the level.+-- That's better than scaling AI sofistication down based on the FPS setting+-- and machine speed.+maxFps :: Int+maxFps = 15++-- | Maximal polls per second.+maxPolls :: Int+maxPolls = let maxP = 120+ in assert (maxP >= 2 * maxFps `blame` (maxP, maxFps)) maxP++-- | Add a given number of microseconds to time.+addTime :: ClockTime -> Int -> ClockTime+addTime (TOD s p) ms = TOD s (p + fromIntegral (ms * 1000000))++-- | The difference between the first and the second time, in microseconds.+diffTime :: ClockTime -> ClockTime -> Int+diffTime (TOD s1 p1) (TOD s2 p2) =+ fromIntegral (s1 - s2) * 1000000 ++ fromIntegral (p1 - p2) `div` 1000000++-- | Poll the frame queue often and draw frames at fixed intervals.+pollFrames :: FrontendSession -> Maybe ClockTime -> IO ()+pollFrames sess (Just setTime) = do+ -- Check if the time is up.+ curTime <- getClockTime+ let diffT = diffTime setTime curTime+ if diffT > 1000000 `div` maxPolls+ then do+ -- Delay half of the time difference.+ threadDelay $ diffTime curTime setTime `div` 2+ pollFrames sess $ Just setTime+ else+ -- Don't delay, because time is up!+ pollFrames sess Nothing+pollFrames sess@FrontendSession{sframeState} Nothing = do+ -- Time is up, check if we actually wait for anyting.+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} ->+ case tryReadLQueue fpushed of+ Just (Just frame, queue) -> do+ -- The frame has arrived so send it for drawing and update delay.+ putMVar sframeState FPushed{fpushed = queue, fshown = frame}+ postGUIAsync $ output sess frame+ curTime <- getClockTime+ threadDelay $ 1000000 `div` (maxFps * 2)+ pollFrames sess $ Just $ addTime curTime $ 1000000 `div` maxFps+ Just (Nothing, queue) -> do+ -- Delay requested via an empty frame.+ putMVar sframeState FPushed{fpushed = queue, ..}+ curTime <- getClockTime+ -- There is no problem if the delay is a bit delayed.+ threadDelay $ 1000000 `div` maxFps+ pollFrames sess $ Just $ addTime curTime $ 1000000 `div` maxFps+ Nothing -> do+ -- The queue is empty, the game logic thread lags.+ putMVar sframeState fs+ -- Time is up, the game thread is going to send a frame,+ -- (otherwise it would change the state), so poll often.+ threadDelay $ 1000000 `div` maxPolls+ pollFrames sess Nothing+ _ -> do+ putMVar sframeState fs+ -- Not in the Push state, so poll lazily to catch the next state change.+ -- The slow polling also gives the game logic a head start+ -- in creating frames in case one of the further frames is slow+ -- to generate and would normally cause a jerky delay in drawing.+ threadDelay $ 1000000 `div` (maxFps * 2)+ pollFrames sess Nothing++-- | Add a game screen frame to the frame drawing channel, or show+-- it ASAP if @immediate@ display is requested and the channel is empty.+pushFrame :: FrontendSession -> Bool -> Bool -> Maybe SingleFrame -> IO ()+pushFrame sess noDelay immediate rawFrame = do+ let FrontendSession{sframeState, slastFull} = sess+ -- Full evaluation is done outside the mvar locks.+ let !frame = case rawFrame of+ Nothing -> Nothing+ Just fr -> Just $! evalFrame sess fr+ -- Lock frame addition.+ (lastFrame, anyFollowed) <- takeMVar slastFull+ -- Comparison of frames is done outside the frame queue mvar lock.+ let nextFrame = if frame == Just lastFrame+ then Nothing -- no sense repeating+ else frame+ -- Lock frame queue.+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} ->+ putMVar sframeState+ $ if isNothing nextFrame && anyFollowed+ then fs -- old news+ else FPushed{fpushed = writeLQueue fpushed nextFrame, ..}+ FNone | immediate -> do+ -- If the frame not repeated, draw it.+ maybe skip (postGUIAsync . output sess) nextFrame+ putMVar sframeState FNone+ FNone ->+ -- Never start playing with an empty frame.+ let fpushed = if isJust nextFrame+ then writeLQueue newLQueue nextFrame+ else newLQueue+ fshown = dummyFrame+ in putMVar sframeState FPushed{..}+ case nextFrame of+ Nothing -> putMVar slastFull (lastFrame, True)+ 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+ ff ll l = reverse (foldl' f [] l) : ll+ f l ac = let !tag = stags M.! Color.acAttr ac in tag : l+ in GtkFrame{..}++-- | Trim current frame queue and display the most recent frame, if any.+trimFrameState :: FrontendSession -> IO ()+trimFrameState sess@FrontendSession{sframeState} = do+ -- Take the lock to wipe out the frame queue, unless it's empty already.+ fs <- takeMVar sframeState+ case fs of+ FPushed{..} ->+ -- Remove all but the last element of the frame queue.+ -- The kept (and displayed) last element ensures that+ -- @slastFull@ is not invalidated.+ case lastLQueue fpushed of+ Just frame -> do+ -- Comparison is done inside the mvar lock, this time, but it's OK,+ -- since we wipe out the queue anyway, not draw it concurrently.+ let lastFrame = fshown+ nextFrame = if frame == lastFrame+ then Nothing -- no sense repeating+ else Just frame+ -- Draw the last frame ASAP.+ maybe skip (postGUIAsync . output sess) nextFrame+ Nothing -> return ()+ FNone -> return ()+ -- Wipe out the frame queue. Release the lock.+ putMVar sframeState FNone++-- | Add a frame to be drawn.+display :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()+display sess noDelay = pushFrame sess noDelay False++-- | Display a prompt, wait for any key.+-- Starts in Push or None mode, stop in None mode.+promptGetAnyKey :: FrontendSession -> SingleFrame -> IO K.KM+promptGetAnyKey sess@FrontendSession{schanKey} frame = do+ pushFrame sess True True $ Just frame+ km <- readChan schanKey+ trimFrameState sess+ return km++-- | Tells a dead key.+deadKey :: String -> Bool+deadKey x = case x of+ "Shift_R" -> True+ "Shift_L" -> True+ "Control_L" -> True+ "Control_R" -> True+ "Super_L" -> True+ "Super_R" -> True+ "Menu" -> True+ "Alt_L" -> True+ "Alt_R" -> True+ "ISO_Level2_Shift" -> True+ "ISO_Level3_Shift" -> True+ "ISO_Level2_Latch" -> True+ "ISO_Level3_Latch" -> True+ "Num_Lock" -> True+ "Caps_Lock" -> True+ _ -> False++-- | Translates modifiers to our own encoding.+modifierTranslate :: [Modifier] -> K.Modifier+modifierTranslate mods =+ if Control `elem` mods then K.Control else K.NoModifier++doAttr :: TextTag -> Color.Attr -> IO ()+doAttr tt attr@Color.Attr{fg, bg}+ | attr == Color.defAttr = return ()+ | fg == Color.defFG = set tt [textTagBackground := Color.colorToRGB bg]+ | bg == Color.defBG = set tt [textTagForeground := Color.colorToRGB fg]+ | otherwise = set tt [textTagForeground := Color.colorToRGB fg,+ textTagBackground := Color.colorToRGB bg]
+ Game/LambdaHack/Frontend/Std.hs view
@@ -0,0 +1,107 @@+-- | Text frontend based on stdin/stdout, intended for bots.+module Game.LambdaHack.Frontend.Std+ ( -- * Session data type for the frontend+ FrontendSession+ -- * The output and input operations+ , display, promptGetAnyKey+ -- * Frontend administration tools+ , frontendName, startup+ ) where++import qualified Data.ByteString.Char8 as BS+import qualified Data.List as L+import Data.Text.Encoding (encodeUtf8)+import qualified System.IO as SIO++import Game.LambdaHack.Common.Animation (SingleFrame (..))+import qualified Game.LambdaHack.Common.Color as Color+import qualified Game.LambdaHack.Common.Key as K (KM (..), Key (..),+ Modifier (..))++-- | No session data needs to be maintained by this frontend.+type FrontendSession = ()++-- | The name of the frontend.+frontendName :: String+frontendName = "std"++-- | Starts the main program loop using the frontend input and output.+startup :: String -> (FrontendSession -> IO ()) -> IO ()+startup _ k = k ()++-- | Output to the screen via the frontend.+display :: FrontendSession -- ^ frontend session data+ -> Bool+ -> Maybe SingleFrame -- ^ the screen frame to draw+ -> IO ()+display _ _ Nothing = return ()+display _ _ (Just SingleFrame{..}) =+ let chars = L.map (BS.pack . L.map Color.acChar) sfLevel+ bs = [encodeUtf8 sfTop, BS.empty] ++ chars ++ [encodeUtf8 sfBottom, BS.empty]+ in mapM_ BS.putStrLn bs++-- | Input key via the frontend.+nextEvent :: FrontendSession -> Maybe Bool -> IO K.KM+nextEvent sess mb = do+ e <- BS.hGet SIO.stdin 1+ let c = BS.head e+ if c == '\n' -- let \n mark the end of input, for human players+ then nextEvent sess mb+ else return K.KM {key = keyTranslate c, modifier = K.NoModifier}++-- | Display a prompt, wait for any key.+promptGetAnyKey :: FrontendSession -> SingleFrame+ -> IO K.KM+promptGetAnyKey sess frame = do+ display sess True $ Just frame+ nextEvent sess Nothing++-- HACK: Special translation that block commands the bots should not use+-- and multiplies some other commands.+keyTranslate :: Char -> K.Key+keyTranslate e =+ case e of+ -- Translate some special keys (use vi keys to move).+ '\ESC' -> K.Esc+ '\n' -> K.Return+ '\r' -> K.Return+ '\t' -> K.Tab+ -- For bots: disable purely UI commands.+ 'P' -> K.Char 'U'+ 'V' -> K.Char 'Y'+ 'O' -> K.Char 'J'+ 'I' -> K.Char 'L'+ 'R' -> K.Char 'K'+ '?' -> K.Char 'N'+ -- For bots: don't let them give up, write files, procrastinate.+ 'Q' -> K.Char 'H'+ 'X' -> K.Char 'B'+ 'D' -> K.Return+ '.' -> K.Return+ -- For bots (assuming they go from '0' to 'z'): major commands.+ '<' -> K.Char 'q' -- ban ascending to speed up descending+ '>' -> K.Char '>'+ 'c' -> K.Char 'c'+ 'd' -> K.Char 'r' -- don't let bots drop stuff+ 'g' -> K.Char 'g'+ 'i' -> K.Char 'i'+ 'o' -> K.Char 'o'+ 'q' -> K.Char 'q'+ 'r' -> K.Char 'r'+ 't' -> K.Char 'g' -- tagetting is too hard, so don't throw+ 'z' -> K.Char 'g' -- and don't zap+ 'p' -> K.Char 'g' -- and don't project+ 'a' -> K.Esc -- and don't apply+ -- For bots: minor commands. Targeting is too hard, so don't do it.+ '*' -> K.Char 'c'+ '/' -> K.Char 'c'+ '[' -> K.Char 'g'+ ']' -> K.Char 'g'+ '{' -> K.Char 'g'+ '}' -> K.Char 'g'+ -- Hack for bots: dump config at the start.+ ' ' -> K.Char 'D'+ -- Movement and hero selection.+ c | c `elem` "kjhlyubnKJHLYUBN" -> K.Char c+ c | c `elem` ['0'..'9'] -> K.Char c+ _ -> K.Char '>' -- try hard to descend
+ Game/LambdaHack/Frontend/Vty.hs view
@@ -0,0 +1,129 @@+-- | Text frontend based on Vty.+module Game.LambdaHack.Frontend.Vty+ ( -- * Session data type for the frontend+ FrontendSession+ -- * The output and input operations+ , display, promptGetAnyKey+ -- * Frontend administration tools+ , 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 (SingleFrame (..))+import qualified Game.LambdaHack.Common.Color as Color+import qualified Game.LambdaHack.Common.Key as K (KM (..), Key (..),+ Modifier (..))++-- | Session data maintained by the frontend.+type FrontendSession = Vty++-- | The name of the frontend.+frontendName :: String+frontendName = "vty"++-- | Starts the main program loop using the frontend input and output.+startup :: String -> (FrontendSession -> IO ()) -> IO ()+startup _ k = do+ fs <- mkVty+ k fs+ Vty.shutdown fs++-- | Output to the screen via the frontend.+display :: FrontendSession -- ^ frontend session data++ -> Bool+ -> Maybe SingleFrame -- ^ the screen frame to draw+ -> IO ()+display _ _ Nothing = return ()+display vty _ (Just SingleFrame{..}) =+ let img = (foldr (<->) empty_image+ . L.map (foldr (<|>) empty_image+ . L.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)+ in update vty pic++-- | Input key via the frontend.+nextEvent :: FrontendSession -> Maybe Bool -> IO K.KM+nextEvent sess mb = do+ e <- next_event sess+ case e of+ EvKey n mods -> do+ let key = keyTranslate n+ modifier = modifierTranslate mods+ return K.KM {key, modifier}+ _ -> nextEvent sess mb++-- | Display a prompt, wait for any key.+promptGetAnyKey :: FrontendSession -> SingleFrame+ -> IO K.KM+promptGetAnyKey sess frame = do+ display sess True $ Just frame+ nextEvent sess Nothing++keyTranslate :: Key -> K.Key+keyTranslate n =+ case n of+ KEsc -> K.Esc+ KEnter -> K.Return+ (KASCII ' ') -> K.Space+ (KASCII '\t') -> K.Tab+ KBackTab -> K.BackTab+ KUp -> K.Up+ KDown -> K.Down+ KLeft -> K.Left+ KRight -> K.Right+ KHome -> K.Home+ KPageUp -> K.PgUp+ KEnd -> K.End+ KPageDown -> K.PgDn+ KBegin -> K.Begin+ (KASCII c) -> K.Char c+ _ -> K.Unknown (show n)++-- | Translates modifiers to our own encoding.+modifierTranslate :: [Modifier] -> K.Modifier+modifierTranslate mods =+ if MCtrl `elem` mods then K.Control else K.NoModifier++-- 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.+hack :: Color.Color -> Attr -> Attr+hack c a = if Color.isBright c then with_style a bold else a++setAttr :: Color.Attr -> Attr+setAttr Color.Attr{fg, bg} =+-- This optimization breaks display for white background terminals:+-- if (fg, bg) == Color.defAttr+-- then def_attr+-- else+ hack fg $ hack bg $+ def_attr { attr_fore_color = SetTo (aToc fg)+ , attr_back_color = SetTo (aToc bg) }++aToc :: Color.Color -> Color+aToc Color.Black = black+aToc Color.Red = red+aToc Color.Green = green+aToc Color.Brown = yellow+aToc Color.Blue = blue+aToc Color.Magenta = magenta+aToc Color.Cyan = cyan+aToc Color.White = white+aToc Color.BrBlack = bright_black+aToc Color.BrRed = bright_red+aToc Color.BrGreen = bright_green+aToc Color.BrYellow = bright_yellow+aToc Color.BrBlue = bright_blue+aToc Color.BrMagenta = bright_magenta+aToc Color.BrCyan = bright_cyan+aToc Color.BrWhite = bright_white
− Game/LambdaHack/Item.hs
@@ -1,254 +0,0 @@--- | Weapons, treasure and all the other items in the game.--- No operation in this module--- involves the 'State' or 'Action' type.--- TODO: Document after it's rethought and rewritten wrt separating--- inventory manangement and items proper.-module Game.LambdaHack.Item- ( -- * Teh @Item@ type- Item(..), newItem, viewItem, itemPrice- -- * Inventory search- , strongestSearch, strongestSword, strongestRegen- -- * Inventory management- , joinItem, removeItemByLetter, equalItemIdentity, removeItemByIdentity- , assignLetter- -- * Inventory symbol operations- , letterLabel, cmpLetterMaybe, maxLetter, letterRange- -- * The @FlavourMap@ type- , FlavourMap, getFlavour, dungeonFlavourMap- -- * The @Discoveries@ type- , Discoveries- ) where--import Data.Binary-import qualified Data.Set as S-import qualified Data.List as L-import qualified Data.Map as M-import Data.Maybe-import Data.Char-import Data.Function-import Data.Ord-import Control.Monad-import Data.Text (Text)-import qualified Data.Text as T-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Random-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Content.RuleKind-import qualified Game.LambdaHack.Color as Color-import Game.LambdaHack.Flavour-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Effect-import Game.LambdaHack.Msg---- TODO: see the TODO about ipower in ItemKind.--- TODO: define type InvSymbol = Char and move all ops to another file.--- TODO: perhaps remove jletter and jcount? Should inventory semantics--- be separate from item semantics?--- TODO: the list resulting from joinItem can contain items--- with the same letter.--- TODO: name [Item] Inventory and have some invariants, e.g. no equal letters.--- | Game items in inventories or strewn around the dungeon.-data Item = Item- { jkind :: !(Kind.Id ItemKind) -- ^ kind of the item- , jpower :: !Int -- ^ power of the item- , jletter :: Maybe Char -- ^ inventory symbol- , jcount :: !Int -- ^ inventory count- }- deriving Show--instance Binary Item where- put (Item ik ip il ic ) =- put ik >> put ip >> put il >> put ic- get = liftM4 Item get get get get---- | Generate an item.-newItem :: Kind.Ops ItemKind -> Int -> Int -> Rnd Item-newItem cops@Kind.Ops{opick, okind} lvl depth = do- ikChosen <- opick (T.pack "dng") (const True)- let kind = okind ikChosen- count <- rollDeep lvl depth (icount kind)- if count == 0- then newItem cops lvl depth -- Rare item; beware of inifite loops.- else do- power <- rollDeep lvl depth (ipower kind)- return $ Item ikChosen power (itemLetter kind) count---- | Represent an item on the map.-viewItem :: Kind.Ops ItemKind -> Kind.Id ItemKind -> FlavourMap- -> (Char, Color.Color)-viewItem cops@Kind.Ops{osymbol} ik assocs =- (osymbol ik, flavourToColor $ getFlavour cops assocs ik)---- | Price an item, taking count into consideration.-itemPrice :: Kind.Ops ItemKind -> Item -> Int-itemPrice Kind.Ops{osymbol} i =- case osymbol (jkind i) of- '$' -> jcount i- '*' -> jcount i * 100- _ -> 0---- | The type of already discovered items.-type Discoveries = S.Set (Kind.Id ItemKind)---- TODO: rewrite and move elsewhere--- Could be optimized to IntMap and IntSet, but won't ever be a bottleneck,--- unless we have thousands of item kinds.--- | Flavours assigned to items in this game.-type FlavourMap = M.Map (Kind.Id ItemKind) Flavour---- | Assigns flavours to item kinds. Assures no flavor is repeated,--- except for items with only one permitted flavour.-rollFlavourMap :: Kind.Id ItemKind -> ItemKind- -> Rnd (FlavourMap, S.Set Flavour)- -> Rnd (FlavourMap, S.Set Flavour)-rollFlavourMap key ik rnd =- let flavours = iflavour ik- in if L.length flavours == 1- then rnd- else do- (assocs, available) <- rnd- let proper = S.fromList flavours `S.intersection` available- flavour <- oneOf (S.toList proper)- return (M.insert key flavour assocs, S.delete flavour available)---- | Randomly chooses flavour for all item kinds for this game.-dungeonFlavourMap :: Kind.Ops ItemKind -> Rnd FlavourMap-dungeonFlavourMap Kind.Ops{ofoldrWithKey} =- liftM fst $- ofoldrWithKey rollFlavourMap (return (M.empty, S.fromList stdFlav))--getFlavour :: Kind.Ops ItemKind -> FlavourMap -> Kind.Id ItemKind -> Flavour-getFlavour Kind.Ops{okind} assocs ik =- let kind = okind ik- in case iflavour kind of- [] -> assert `failure` (assocs, ik, kind)- [f] -> f- _:_ -> assocs M.! ik--itemLetter :: ItemKind -> Maybe Char-itemLetter ik = if isymbol ik == '$' then Just '$' else Nothing---- | Assigns a letter to an item, for inclusion--- in the inventory of a hero. Takes a remembered--- letter and a starting letter.-assignLetter :: Maybe Char -> Char -> [Item] -> Maybe Char-assignLetter r c is =- case r of- Just l | l `elem` allowed -> Just l- _ -> listToMaybe free- where- current = S.fromList (mapMaybe jletter is)- allLetters = ['a'..'z'] ++ ['A'..'Z']- candidates = take (length allLetters) $- drop (fromJust (L.findIndex (== c) allLetters)) $- cycle allLetters- free = L.filter (\x -> not (x `S.member` current)) candidates- allowed = '$' : free--cmpLetter :: Char -> Char -> Ordering-cmpLetter x y = compare (isUpper x, toLower x) (isUpper y, toLower y)--cmpLetterMaybe :: Maybe Char -> Maybe Char -> Ordering-cmpLetterMaybe Nothing Nothing = EQ-cmpLetterMaybe Nothing (Just _) = GT-cmpLetterMaybe (Just _) Nothing = LT-cmpLetterMaybe (Just l) (Just l') = cmpLetter l l'--maxBy :: (a -> a -> Ordering) -> a -> a -> a-maxBy cmp x y = case cmp x y of- LT -> y- _ -> x--maxLetter :: Char -> Char -> Char-maxLetter = maxBy cmpLetter--mergeLetter :: Maybe Char -> Maybe Char -> Maybe Char-mergeLetter = mplus--letterRange :: [Char] -> Text-letterRange ls =- sectionBy (L.sortBy cmpLetter ls) Nothing- where- succLetter c d = ord d - ord c == 1-- sectionBy [] Nothing = T.empty- sectionBy [] (Just (c,d)) = finish (c,d)- sectionBy (x:xs) Nothing = sectionBy xs (Just (x,x))- sectionBy (x:xs) (Just (c,d))- | succLetter d x = sectionBy xs (Just (c,x))- | otherwise = finish (c,d) <> sectionBy xs (Just (x,x))-- finish (c,d) | c == d = T.pack [c]- | succLetter c d = T.pack $ [c, d]- | otherwise = T.pack $ [c, '-', d]--letterLabel :: Maybe Char -> MU.Part-letterLabel Nothing = MU.Text $ T.pack " "-letterLabel (Just c) = MU.Text $ T.pack $ c : " -"---- | Adds an item to a list of items, joining equal items.--- Also returns the joined item.-joinItem :: Item -> [Item] -> (Item, [Item])-joinItem i is =- case findItem (equalItemIdentity i) is of- Nothing -> (i, i : is)- Just (j,js) -> let n = i { jcount = jcount i + jcount j,- jletter = mergeLetter (jletter j) (jletter i) }- in (n, n : js)---- | Removes an item from a list of items.--- Takes an equality function (i.e., by letter or ny kind) as an argument.-removeItemBy :: (Item -> Item -> Bool) -> Item -> [Item] -> [Item]-removeItemBy eq i = concatMap $ \ x ->- if eq i x- then let remaining = jcount x - jcount i- in if remaining > 0- then [x { jcount = remaining }]- else []- else [x]--equalItemIdentity :: Item -> Item -> Bool-equalItemIdentity i1 i2 = jpower i1 == jpower i2 && jkind i1 == jkind i2--removeItemByIdentity :: Item -> [Item] -> [Item]-removeItemByIdentity = removeItemBy equalItemIdentity--equalItemLetter :: Item -> Item -> Bool-equalItemLetter = (==) `on` jletter--removeItemByLetter :: Item -> [Item] -> [Item]-removeItemByLetter = removeItemBy equalItemLetter---- | Finds an item in a list of items.-findItem :: (Item -> Bool) -> [Item] -> Maybe (Item, [Item])-findItem p =- findItem' []- where- findItem' _ [] = Nothing- findItem' acc (i:is)- | p i = Just (i, reverse acc ++ is)- | otherwise = findItem' (i:acc) is--strongestItem :: [Item] -> (Item -> Bool) -> Maybe Item-strongestItem is p =- let cmp = comparing jpower- igs = L.filter p is- in case igs of- [] -> Nothing- _ -> Just $ L.maximumBy cmp igs--strongestSearch :: Kind.Ops ItemKind -> [Item] -> Maybe Item-strongestSearch Kind.Ops{okind} bitems =- strongestItem bitems $ \ i -> (ieffect $ okind $ jkind i) == Searching---- TODO: generalise, in particular take base damage into account-strongestSword :: Kind.COps -> [Item] -> Maybe Item-strongestSword Kind.COps{coitem=Kind.Ops{osymbol}, corule} bitems =- strongestItem bitems $ \ i -> (osymbol $ jkind i)- `elem` (ritemMelee $ Kind.stdRuleset corule)--strongestRegen :: Kind.Ops ItemKind -> [Item] -> Maybe Item-strongestRegen Kind.Ops{okind} bitems =- strongestItem bitems $ \ i -> (ieffect $ okind $ jkind i) == Regeneration
− Game/LambdaHack/ItemAction.hs
@@ -1,525 +0,0 @@-{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}-{-# OPTIONS_GHC -fno-warn-type-defaults #-}--- | Item UI code with the 'Action' type and everything it depends on--- that is not already in Action.hs and EffectAction.hs.--- This file should not depend on Actions.hs.--- TODO: Add an export list and document after it's rewritten according to #17.-module Game.LambdaHack.ItemAction where--import Control.Monad-import Control.Monad.State hiding (State, state)-import qualified Data.List as L-import qualified Data.IntMap as IM-import Data.Maybe-import Data.Ord-import qualified Data.IntSet as IS-import Data.Text (Text)-import qualified Data.Text as T-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Action-import Game.LambdaHack.Point-import Game.LambdaHack.Item-import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Level-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import Game.LambdaHack.Perception-import Game.LambdaHack.State-import Game.LambdaHack.EffectAction-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Time-import Game.LambdaHack.Msg--default (Text)---- TODO: When inventory is displayed, let TAB switch the player (without--- announcing that) and show the inventory of the new player.--- | Display inventory-inventory :: ActionFrame ()-inventory = do- Kind.COps{coactor} <- getCOps- pbody <- gets getPlayerBody- items <- gets getPlayerItem- if L.null items- then abortWith $ makeSentence- [ MU.SubjectVerbSg (partActor coactor pbody) "be"- , "not carrying anything" ]- else do- io <- itemOverlay True False items- let blurb = makePhrase [MU.Capitalize $- MU.SubjectVerbSg (partActor coactor pbody) "be carrying:"]- displayOverlays blurb "" io---- | Let the player choose any item with a given group name.--- Note that this does not guarantee the chosen item belongs to the group,--- as the player can override the choice.-getGroupItem :: [Item] -- ^ all objects in question- -> MU.Part -- ^ name of the group- -> [Char] -- ^ accepted item symbols- -> Text -- ^ prompt- -> Text -- ^ how to refer to the collection of objects- -> Action Item-getGroupItem is object syms prompt packName = do- Kind.COps{coitem=Kind.Ops{osymbol}} <- getCOps- let choice i = osymbol (jkind i) `elem` syms- header = makePhrase [MU.Capitalize (MU.Ws object)]- getItem prompt choice header is packName--applyGroupItem :: ActorId -- ^ actor applying the item (is on current level)- -> MU.Part -- ^ how the applying is called- -> Item -- ^ the item to be applied- -> Action ()-applyGroupItem actor verb item = do- Kind.COps{coactor, coitem} <- getCOps- state <- get- body <- gets (getActor actor)- per <- getPerception- -- only one item consumed, even if several in inventory- let consumed = item { jcount = 1 }- msg = makeSentence- [ MU.SubjectVerbSg (partActor coactor body) verb- , partItemNWs coitem state consumed ]- loc = bloc body- removeFromInventory actor consumed loc- when (loc `IS.member` totalVisible per) $ msgAdd msg- itemEffectAction 5 actor actor consumed False--playerApplyGroupItem :: MU.Part -> MU.Part -> [Char] -> Action ()-playerApplyGroupItem verb object syms = do- Kind.COps{coitem=Kind.Ops{okind}} <- getCOps- is <- gets getPlayerItem- item <- getGroupItem is object syms- (makePhrase ["What to", verb MU.:> "?"]) "in inventory"- pl <- gets splayer- applyGroupItem pl (iverbApply $ okind $ jkind item) item--projectGroupItem :: ActorId -- ^ actor projecting the item (is on current lvl)- -> Point -- ^ target location of the projectile- -> MU.Part -- ^ how the projecting is called- -> Item -- ^ the item to be projected- -> Action ()-projectGroupItem source tloc _verb item = do- cops@Kind.COps{coactor, coitem} <- getCOps- state <- get- sm <- gets (getActor source)- per <- getPerception- pl <- gets splayer- Actor{btime} <- gets getPlayerBody- lvl <- gets slevel- ceps <- gets (ceps . scursor)- lxsize <- gets (lxsize . slevel)- lysize <- gets (lysize . slevel)- sfaction <- gets sfaction- let consumed = item { jcount = 1 }- sloc = bloc sm- svisible = sloc `IS.member` totalVisible per- subject =- if svisible- then sm- else sm {bname = Just "somebody"}- -- When projecting, the first turn is spent aiming.- -- The projectile is seen one tile from the actor, giving a hint- -- about the aim and letting the target evade.- msg = makeSentence- [ MU.SubjectVerbSg (partActor coactor subject) "aim"- , partItemNWs coitem state consumed ]- -- TODO: AI should choose the best eps.- eps = if source == pl then ceps else 0- -- Setting monster's projectiles time to player time ensures- -- the projectile covers the whole normal distance already the first- -- turn that the player observes it moving. This removes- -- the possibility of micromanagement by, e.g., waiting until- -- the first distance is short.- -- When the monster faction has its selected player, hero player's- -- projectiles should be set to the time of the opposite party as well.- -- Both parties would see their own projectiles move part of the way- -- and the opposite party's projectiles waiting one turn.- btimeDelta = timeAddFromSpeed coactor sm btime- time =- if bfaction sm == sfaction || source == pl- then btimeDelta `timeAdd` timeNegate timeClip- else btime- bl = bla lxsize lysize eps sloc tloc- case bl of- Nothing -> abortWith "cannot zap oneself"- Just [] -> assert `failure` (sloc, tloc, "project from the edge of level")- Just path@(loc:_) -> do- let projVis = loc `IS.member` totalVisible per- removeFromInventory source consumed sloc- inhabitants <- gets (locToActor loc)- if accessible cops lvl sloc loc && isNothing inhabitants- then- modify $ addProjectile cops consumed loc (bfaction sm) path time- else- abortWith "blocked"- when (svisible || projVis) $ msgAdd msg--playerProjectGroupItem :: MU.Part -> MU.Part -> [Char] -> ActionFrame ()-playerProjectGroupItem verb object syms = do- ms <- gets hostileList- lxsize <- gets (lxsize . slevel)- lysize <- gets (lysize . slevel)- ploc <- gets (bloc . getPlayerBody)- if foesAdjacent lxsize lysize ploc ms- then abortWith "You can't aim in melee."- else playerProjectGI verb object syms--playerProjectGI :: MU.Part -> MU.Part -> [Char] -> ActionFrame ()-playerProjectGI verb object syms = do- state <- get- pl <- gets splayer- ploc <- gets (bloc . getPlayerBody)- per <- getPerception- let retarget msg = do- msgAdd msg- let upd cursor = cursor {clocation=ploc, ceps=0}- modify (updateCursor upd)- frs <- targetMonster TgtAuto- -- Mark that unexpectedly it does not take time.- modify (\ s -> s {stakeTime = Just False})- return frs- case targetToLoc (totalVisible per) state ploc of- Just loc -> do- Kind.COps{coitem=Kind.Ops{okind}} <- getCOps- is <- gets getPlayerItem- item <- getGroupItem is object syms- (makePhrase ["What to", verb MU.:> "?"]) "in inventory"- targeting <- gets (ctargeting . scursor)- when (targeting == TgtAuto) $ endTargeting True- projectGroupItem pl loc (iverbProject $ okind $ jkind item) item- returnNoFrame ()- Nothing -> retarget "Last target invalid."---- | Start the monster targeting mode. Cycle between monster targets.-targetMonster :: TgtMode -> ActionFrame ()-targetMonster tgtMode = do- pl <- gets splayer- ploc <- gets (bloc . getPlayerBody)- sfaction <- gets sfaction- ms <- gets (hostileAssocs sfaction . slevel)- per <- getPerception- lxsize <- gets (lxsize . slevel)- target <- gets (btarget . getPlayerBody)- targeting <- gets (ctargeting . scursor)- -- TODO: sort monsters by distance to the player.- let plms = L.filter ((/= pl) . fst) ms -- don't target yourself- ordLoc (_, m) = (chessDist lxsize ploc $ bloc m, bloc m)- dms = L.sortBy (comparing ordLoc) plms- (lt, gt) = case target of- TEnemy n _ | targeting /= TgtOff -> -- pick the next monster- let i = fromMaybe (-1) $ L.findIndex ((== n) . fst) dms- in L.splitAt (i + 1) dms- TEnemy n _ -> -- try to retarget the old monster- let i = fromMaybe (-1) $ L.findIndex ((== n) . fst) dms- in L.splitAt i dms- _ -> (dms, []) -- target first monster (e.g., number 0)- gtlt = gt ++ lt- seen (_, m) =- let mloc = bloc m- in mloc `IS.member` totalVisible per -- visible by any- && actorReachesLoc pl mloc per (Just pl) -- reachable by player- lf = L.filter seen gtlt- tgt = case lf of- [] -> target -- no monsters in sight, stick to last target- (na, nm) : _ -> TEnemy na (bloc nm) -- pick the next- -- Register the chosen monster, to pick another on next invocation.- updatePlayerBody (\ p -> p { btarget = tgt })- setCursor tgtMode---- | Start the floor targeting mode or reset the cursor location to the player.-targetFloor :: TgtMode -> ActionFrame ()-targetFloor tgtMode = do- ploc <- gets (bloc . getPlayerBody)- target <- gets (btarget . getPlayerBody)- targeting <- gets (ctargeting . scursor)- let tgt = case target of- TEnemy _ _ -> TCursor -- forget enemy target, keep the cursor- _ | targeting /= TgtOff -> TLoc ploc -- double key press: reset cursor- TPath _ -> TCursor- t -> t -- keep the target from previous targeting session- -- Register that we want to target only locations.- updatePlayerBody (\ p -> p { btarget = tgt })- setCursor tgtMode---- | Set, activate and display cursor information.-setCursor :: TgtMode -> ActionFrame ()-setCursor tgtMode = assert (tgtMode /= TgtOff) $ do- state <- get- per <- getPerception- ploc <- gets (bloc . getPlayerBody)- clocLn <- gets slid- let upd cursor@Cursor{ctargeting, clocation=clocationOld, ceps=cepsOld} =- let clocation =- fromMaybe ploc (targetToLoc (totalVisible per) state ploc)- ceps = if clocation == clocationOld then cepsOld else 0- newTgtMode = if ctargeting == TgtOff then tgtMode else ctargeting- in cursor { ctargeting = newTgtMode, clocation, clocLn, ceps }- modify (updateCursor upd)- doLook---- | Tweak the @eps@ parameter of the targeting digital line.-epsIncr :: Bool -> Action ()-epsIncr b = do- targeting <- gets (ctargeting . scursor)- if targeting /= TgtOff- then modify $ updateCursor $- \ c@Cursor{ceps} -> c {ceps = ceps + if b then 1 else -1}- else neverMind True -- no visual feedback, so no sense---- | End targeting mode, accepting the current location or not.-endTargeting :: Bool -> Action ()-endTargeting accept = do- returnLn <- gets (creturnLn . scursor)- target <- gets (btarget . getPlayerBody)- per <- getPerception- cloc <- gets (clocation . scursor)- sfaction <- gets sfaction- ms <- gets (hostileAssocs sfaction . slevel)- -- Return to the original level of the player. Note that this can be- -- a different level than the one we started targeting at,- -- if the player was changed while targeting.- switchLevel returnLn- modify (updateCursor (\ c -> c { ctargeting = TgtOff }))- when accept $ do- case target of- TEnemy _ _ -> do- -- If in monster targeting mode, switch to the monster under- -- the current cursor location, if any.- let canSee = IS.member cloc (totalVisible per)- when (accept && canSee) $- case L.find (\ (_im, m) -> bloc m == cloc) ms of- Just (im, m) ->- let tgt = TEnemy im (bloc m)- in updatePlayerBody (\ p -> p { btarget = tgt })- Nothing -> return ()- _ -> updatePlayerBody (\ p -> p { btarget = TLoc cloc })- if accept- then endTargetingMsg- else msgAdd "targeting canceled"--endTargetingMsg :: Action ()-endTargetingMsg = do- Kind.COps{coactor} <- getCOps- pbody <- gets getPlayerBody- state <- get- lxsize <- gets (lxsize . slevel)- let targetMsg = case btarget pbody of- TEnemy a _ll ->- if memActor a state- then partActor coactor $ getActor a state- else "a fear of the past"- TLoc loc -> MU.Text $ "location" <+> showPoint lxsize loc- TPath _ -> "a path"- TCursor -> "current cursor position continuously"- msgAdd $ makeSentence- [MU.SubjectVerbSg (partActor coactor pbody) "target", targetMsg]---- | Cancel something, e.g., targeting mode, resetting the cursor--- to the position of the player. Chosen target is not invalidated.-cancelCurrent :: ActionFrame () -> ActionFrame ()-cancelCurrent h = do- targeting <- gets (ctargeting . scursor)- if targeting /= TgtOff- then inFrame $ endTargeting False- else h -- nothing to cancel right now, treat this as a command invocation---- | Accept something, e.g., targeting mode, keeping cursor where it was.--- Or perform the default action, if nothing needs accepting.-acceptCurrent :: ActionFrame () -> ActionFrame ()-acceptCurrent h = do- targeting <- gets (ctargeting . scursor)- if targeting /= TgtOff- then inFrame $ endTargeting True- else h -- nothing to accept right now, treat this as a command invocation---- | Clear current messages, show the next screen if any.-clearCurrent :: Action ()-clearCurrent = return ()---- | Drop a single item.-dropItem :: Action ()-dropItem = do- -- TODO: allow dropping a given number of identical items.- Kind.COps{coactor, coitem} <- getCOps- pl <- gets splayer- state <- get- pbody <- gets getPlayerBody- ploc <- gets (bloc . getPlayerBody)- ims <- gets getPlayerItem- stack <- getAnyItem "What to drop?" ims "in inventory"- let item = stack { jcount = 1 }- removeOnlyFromInventory pl item (bloc pbody)- msgAdd $ makeSentence- [ MU.SubjectVerbSg (partActor coactor pbody) "drop"- , partItemNWs coitem state item ]- modify (updateLevel (dropItemsAt [item] ploc))---- TODO: this is a hack for dropItem, because removeFromInventory--- makes it impossible to drop items if the floor not empty.-removeOnlyFromInventory :: ActorId -> Item -> Point -> Action ()-removeOnlyFromInventory actor i _loc =- modify (updateAnyActorItem actor (removeItemByLetter i))---- | Remove given item from an actor's inventory or floor.--- TODO: this is subtly wrong: if identical items are on the floor and in--- inventory, the floor one will be chosen, regardless of player intention.--- TODO: right now it ugly hacks (with the ploc) around removing items--- of dead heros/monsters. The subtle incorrectness helps here a lot,--- because items of dead heroes land on the floor, so we use them up--- in inventory, but remove them after use from the floor.-removeFromInventory :: ActorId -> Item -> Point -> Action ()-removeFromInventory actor i loc = do- b <- removeFromLoc i loc- unless b $- modify (updateAnyActorItem actor (removeItemByLetter i))---- | Remove given item from the given location. Tell if successful.-removeFromLoc :: Item -> Point -> Action Bool-removeFromLoc i loc = do- lvl <- gets slevel- if not $ L.any (equalItemIdentity i) (lvl `atI` loc)- then return False- else- modify (updateLevel (updateIMap adj)) >>- return True- where- rib Nothing = assert `failure` (i, loc)- rib (Just (is, irs)) =- case (removeItemByIdentity i is, irs) of- ([], []) -> Nothing- iss -> Just iss- adj = IM.alter rib loc--actorPickupItem :: ActorId -> Action ()-actorPickupItem actor = do- Kind.COps{coactor, coitem} <- getCOps- state <- get- pl <- gets splayer- per <- getPerception- lvl <- gets slevel- body <- gets (getActor actor)- bitems <- gets (getActorItem actor)- let loc = bloc body- perceived = loc `IS.member` totalVisible per- isPlayer = actor == pl- -- check if something is here to pick up- case lvl `atI` loc of- [] -> abortWith "nothing here"- i:is -> -- pick up first item; TODO: let pl select item; not for monsters- case assignLetter (jletter i) (bletter body) bitems of- Just l -> do- let (ni, nitems) = joinItem (i { jletter = Just l }) bitems- -- msg depends on who picks up and if a hero can perceive it- if isPlayer- then msgAdd $ makePhrase [ letterLabel (jletter ni)- , partItemNWs coitem state ni ]- else when perceived $- msgAdd $ makeSentence- [ MU.SubjectVerbSg (partActor coactor body) "pick up"- , partItemNWs coitem state i ]- removeFromLoc i loc- >>= assert `trueM` (i, is, loc, "item is stuck")- -- add item to actor's inventory:- updateAnyActor actor $ \ m ->- m { bletter = maxLetter l (bletter body) }- modify (updateAnyActorItem actor (const nitems))- Nothing -> abortWith "cannot carry any more"--pickupItem :: Action ()-pickupItem = do- pl <- gets splayer- actorPickupItem pl---- TODO: I think that player handlers should be wrappers--- around more general actor handlers, but--- the actor handlers should be performing--- specific actions, i.e., already specify the item to be--- picked up. It doesn't make sense to invoke dialogues--- for arbitrary actors, and most likely the--- decision for a monster is based on perceiving--- a particular item to be present, so it's already--- known. In actor handlers we should make sure--- that messages are printed to the player only if the--- hero can perceive the action.---- TODO: you can drop an item already on the floor, which works correctly,--- but is weird and useless.--allObjectsName :: Text-allObjectsName = "Objects"---- | Let the player choose any item from a list of items.-getAnyItem :: Text -- ^ prompt- -> [Item] -- ^ all items in question- -> Text -- ^ how to refer to the collection of items- -> Action Item-getAnyItem prompt = getItem prompt (const True) allObjectsName--data ItemDialogState = INone | ISuitable | IAll deriving Eq---- | Let the player choose a single, preferably suitable,--- item from a list of items.-getItem :: Text -- ^ prompt message- -> (Item -> Bool) -- ^ which items to consider suitable- -> Text -- ^ how to describe suitable items- -> [Item] -- ^ all items in question- -> Text -- ^ how to refer to the collection of items- -> Action Item-getItem prompt p ptext is0 isn = do- lvl <- gets slevel- body <- gets getPlayerBody- let loc = bloc body- tis = lvl `atI` loc- floorFull = not $ null tis- (floorMsg, floorKey) | floorFull = (", -", [K.Char '-'])- | otherwise = ("", [])- isp = L.filter p is0- bestFull = not $ null isp- (bestMsg, bestKey)- | bestFull =- let bestLetter = maybe "" (\ l -> "(" <> T.singleton l <> ")") $- jletter $ L.maximumBy cmpItemLM isp- in (", RET" <> bestLetter, [K.Return])- | otherwise = ("", [])- cmpItemLM i1 i2 = cmpLetterMaybe (jletter i1) (jletter i2)- keys ims =- let mls = mapMaybe jletter ims- ks = bestKey ++ floorKey ++ [K.Char '?'] ++ map K.Char mls- in zip ks $ repeat K.NoModifier- choice ims =- if null ims- then "[?" <> floorMsg- else let mls = mapMaybe jletter ims- r = letterRange mls- in "[" <> r <> ", ?" <> floorMsg <> bestMsg- ask = do- when (L.null is0 && L.null tis) $- abortWith "Not carrying anything."- perform INone- perform itemDialogState = do- let (ims, imsOver, msg) = case itemDialogState of- INone -> (isp, [], prompt)- ISuitable -> (isp, isp, ptext <+> isn <> ".")- IAll -> (is0, is0, allObjectsName <+> isn <> ".")- io <- itemOverlay True False imsOver- (command, modifier) <-- displayChoiceUI (msg <+> choice ims) io (keys ims)- assert (modifier == K.NoModifier) $- case command 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 $ L.maximumBy cmpItemLM tis- K.Char l | l `elem` mapMaybe jletter ims ->- let mitem = L.find (maybe False (== l) . jletter) ims- in return $ fromJust mitem- K.Return | bestFull ->- return $ L.maximumBy cmpItemLM isp- k -> assert `failure` "perform: unexpected key:" <+> showT k- ask
− Game/LambdaHack/Key.hs
@@ -1,209 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Frontend-independent keyboard input operations.-module Game.LambdaHack.Key- ( Key(..), handleDir, dirAllMoveKey- , moveBinding, keyTranslate, Modifier(..), showKM- ) where--import Prelude hiding (Left, Right)-import qualified Data.List as L-import qualified Data.Char as Char-import Data.Text (Text)-import qualified Data.Text as T-import Data.Binary--import Game.LambdaHack.PointXY-import Game.LambdaHack.Vector-import Game.LambdaHack.Msg---- TODO: if the file grows much larger, split it and move a part to Utils/---- | Frontend-independent datatype to represent keys.-data Key =- Esc- | Return- | Space- | Tab- | BackTab- | PgUp- | PgDn- | Left- | Right- | Up- | Down- | End- | Begin- | Home- | KP !Char -- ^ a keypad key for a character (digits and operators)- | Char !Char -- ^ a single printable character- | Unknown !String -- ^ an unknown key, registered to warn the user- deriving (Ord, Eq)--instance Binary Key where- put Esc = putWord8 0- put Return = putWord8 1- put Space = putWord8 2- put Tab = putWord8 3- put BackTab = putWord8 4- put PgUp = putWord8 5- put PgDn = putWord8 6- put Left = putWord8 7- put Right = putWord8 8- put Up = putWord8 9- put Down = putWord8 10- put End = putWord8 11- put Begin = putWord8 12- put Home = putWord8 13- put (KP c) = putWord8 14 >> put c- put (Char c) = putWord8 15 >> put c- put (Unknown s) = putWord8 16 >> put s- get = do- tag <- getWord8- case tag of- 0 -> return Esc- 1 -> return Return- 2 -> return Space- 3 -> return Tab- 4 -> return BackTab- 5 -> return PgUp- 6 -> return PgDn- 7 -> return Left- 8 -> return Right- 9 -> return Up- 10 -> return Down- 11 -> return End- 12 -> return Begin- 13 -> return Home- 14 -> fmap KP get- 15 -> fmap Char get- 16 -> fmap Unknown get- _ -> fail "no parse (Key)"---- | Our own encoding of modifiers. Incomplete.-data Modifier =- Control- | NoModifier- deriving (Ord, Eq)---- Common and terse names for keys.-showKey :: Key -> Text-showKey (Char c) = T.singleton c-showKey Esc = "ESC"-showKey Return = "RET"-showKey Space = "SPACE"-showKey Tab = "TAB"-showKey BackTab = "SHIFT-TAB"-showKey PgUp = "PGUP"-showKey PgDn = "PGDOWN"-showKey Left = "LEFT"-showKey Right = "RIGHT"-showKey Up = "UP"-showKey Down = "DOWN"-showKey End = "END"-showKey Begin = "BEGIN"-showKey Home = "HOME"-showKey (KP c) = "KEYPAD(" <> T.singleton c <> ")"-showKey (Unknown s) = T.pack s---- | Show a key with a modifier, if any.-showKM :: (Key, Modifier) -> Text-showKM (key, Control) = "CTRL-" <> showKey key-showKM (key, NoModifier) = showKey key--instance Show Key where- show = T.unpack . showKey--dirViChar :: [Char]-dirViChar = ['y', 'k', 'u', 'l', 'n', 'j', 'b', 'h']--dirViMoveKey :: [Key]-dirViMoveKey = map Char dirViChar--dirMoveKey :: [Key]-dirMoveKey = [Home, Up, PgUp, Right, PgDn, Down, End, Left]--dirAllMoveKey :: [Key]-dirAllMoveKey = dirViMoveKey ++ dirMoveKey--dirViRunKey :: [Key]-dirViRunKey = map (Char . Char.toUpper) dirViChar--dirRunKey :: [Key]-dirRunKey = map KP dirNums--_dirAllRunKey :: [Key]-_dirAllRunKey = dirViRunKey ++ dirRunKey--dirNums :: [Char]-dirNums = ['7', '8', '9', '6', '3', '2', '1', '4']--dirHeroKey :: [Key]-dirHeroKey = map Char dirNums---- | Configurable event handler for the direction keys.--- Used for directed commands such as close door.-handleDir :: X -> (Key, Modifier) -> (Vector -> a) -> a -> a-handleDir lxsize (key, NoModifier) h k =- let mvs = moves lxsize- assocs = zip dirAllMoveKey $ mvs ++ mvs- in maybe k h (L.lookup key assocs)-handleDir _lxsize _ _h k = k---- TODO: deduplicate--- | Binding of both sets of movement keys.-moveBinding :: ((X -> Vector) -> a) -> ((X -> Vector) -> a)- -> [((Key, Modifier), (Text, Bool, a))]-moveBinding move run =- let assign f (km, dir) = (km, (T.empty, True, f dir))- rNoModifier = repeat NoModifier- rControl = repeat Control- in map (assign move) (zip (zip dirViMoveKey rNoModifier) movesWidth) ++- map (assign move) (zip (zip dirMoveKey rNoModifier) movesWidth) ++- map (assign run) (zip (zip dirViRunKey rNoModifier) movesWidth) ++- map (assign run) (zip (zip dirRunKey rNoModifier) movesWidth) ++- map (assign run) (zip (zip dirMoveKey rControl) movesWidth) ++- map (assign run) (zip (zip dirRunKey rControl) movesWidth) ++- map (assign run) (zip (zip dirHeroKey rControl) movesWidth)---- | 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.-keyTranslate :: String -> Key-keyTranslate "less" = Char '<'-keyTranslate "greater" = Char '>'-keyTranslate "period" = Char '.'-keyTranslate "colon" = Char ':'-keyTranslate "comma" = Char ','-keyTranslate "question" = Char '?'-keyTranslate "dollar" = Char '$'-keyTranslate "asterisk" = Char '*'-keyTranslate "KP_Multiply" = Char '*'-keyTranslate "slash" = Char '/'-keyTranslate "KP_Divide" = Char '/'-keyTranslate "underscore" = Char '_'-keyTranslate "minus" = Char '-'-keyTranslate "KP_Subtract" = Char '-'-keyTranslate "plus" = Char '+'-keyTranslate "KP_Add" = Char '+'-keyTranslate "bracketleft" = Char '['-keyTranslate "bracketright" = Char ']'-keyTranslate "braceleft" = Char '{'-keyTranslate "braceright" = Char '}'-keyTranslate "Escape" = Esc-keyTranslate "Return" = Return-keyTranslate "space" = Space-keyTranslate "Tab" = Tab-keyTranslate "ISO_Left_Tab" = BackTab-keyTranslate "KP_Up" = Up-keyTranslate "KP_Down" = Down-keyTranslate "KP_Left" = Left-keyTranslate "KP_Right" = Right-keyTranslate "KP_Home" = Home-keyTranslate "KP_End" = End-keyTranslate "KP_Page_Up" = PgUp-keyTranslate "KP_Page_Down" = PgDn-keyTranslate "KP_Begin" = Begin-keyTranslate "KP_Enter" = Return-keyTranslate ['K','P','_',c] = KP c-keyTranslate [c] = Char c-keyTranslate s = Unknown s
− Game/LambdaHack/Kind.hs
@@ -1,177 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes, ScopedTypeVariables, TypeFamilies #-}--- | General content types and operations.-module Game.LambdaHack.Kind- ( -- * General content types- Id, Speedup(..), Ops(..), COps(..), createOps, stdRuleset- -- * Arrays of content identifiers- , Array, (!), (//), listArray, array, bounds, foldlArray- ) where--import Data.Binary-import qualified Data.List as L-import qualified Data.IntMap as IM-import qualified Data.Map as M-import qualified Data.Word as Word-import qualified Data.Array.Unboxed as A-import qualified Data.Ix as Ix-import Data.Text (Text)-import qualified Data.Text as T-import Game.LambdaHack.Msg-import Data.Maybe (fromMaybe)--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Utils.Frequency-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Content.CaveKind-import Game.LambdaHack.Content.FactionKind-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Content.PlaceKind-import Game.LambdaHack.Content.RuleKind-import Game.LambdaHack.Content.StrategyKind-import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.CDefs-import Game.LambdaHack.Random-import Game.LambdaHack.Misc---- | Content identifiers for the content type @c@.-newtype Id c = Id Word8 deriving (Show, Eq, Ord, Ix.Ix)--instance Binary (Id c) where- put (Id i) = put i- get = fmap Id get---- | 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- }---- | Content operations for the content of type @a@.-data Ops a = Ops- { osymbol :: Id a -> Char -- ^ the symbol of a content element at id- , oname :: Id a -> Text -- ^ the name of a content element at id- , okind :: Id a -> a -- ^ the content element at given id- , ouniqGroup :: Text -> Id a -- ^ the id of the unique member of- -- a singleton content group- , opick :: Text -> (a -> Bool) -> Rnd (Id a)- -- ^ pick a random id belonging to a group- -- and satisfying a predicate- , ofoldrWithKey :: forall b. (Id a -> a -> b -> b) -> b -> b- -- ^ fold over all content elements of @a@- , obounds :: (Id a, Id a) -- ^ bounds of identifiers of content @a@- , ospeedup :: Speedup a -- ^ auxiliary speedup components- }---- | Create content operations for type @a@ from definition of content--- of type @a@.-createOps :: forall a. Show a => CDefs a -> Ops a-createOps CDefs{getSymbol, getName, getFreq, content, validate} =- let kindAssocs :: [(Word.Word8, a)]- kindAssocs = L.zip [0..] content- kindMap :: IM.IntMap a- kindMap = IM.fromDistinctAscList $ L.zip [0..] content- kindFreq :: M.Map Text (Frequency (Id a, a))- kindFreq =- let tuples = [ (group, (n, (Id i, k)))- | (i, k) <- kindAssocs- , (group, n) <- getFreq k, n > 0 ]- f m (group, nik) = M.insertWith (++) group [nik] m- lists = L.foldl' f M.empty tuples- nameFreq group = toFreq $ "opick ('" <> group <> "')"- in M.mapWithKey nameFreq lists- okind (Id i) = fromMaybe (assert `failure` (i, fromEnum i, kindMap))- $ IM.lookup (fromEnum i) kindMap- correct a = not (T.null (getName a)) && L.all ((> 0) . snd) (getFreq a)- offenders = validate content- in assert (allB correct content) $- assert (L.null offenders `blame` ("content not validated: " :: Text,- offenders)) $- Ops- { osymbol = getSymbol . okind- , oname = getName . okind- , okind = okind- , ouniqGroup = \ group ->- let freq = fromMaybe (assert `failure` (group, kindFreq))- $ M.lookup group kindFreq- in case runFrequency freq of- [(n, (i, _))] | n > 0 -> i- l -> assert `failure` l- , opick = \ group p ->- let freq = fromMaybe (assert `failure` (group, kindFreq))- $ M.lookup group kindFreq- in frequency $ do- (i, k) <- freq- breturn (p k) i- {- with MonadComprehensions:- frequency [ i | (i, k) <- kindFreq M.! group, p k ]- -}- , ofoldrWithKey = \ f z -> L.foldr (\ (i, a) -> f (Id i) a) z kindAssocs- , obounds =- let limits = let (i1, a1) = IM.findMin kindMap- (i2, a2) = IM.findMax kindMap- in ((Id (toEnum i1), a1), (Id (toEnum i2), a2))- in (Id 0, (fst . snd) limits)- , ospeedup = undefined -- define elsewhere- }---- | Operations for all content types, gathered together.-data COps = COps- { coactor :: !(Ops ActorKind)- , cocave :: !(Ops CaveKind)- , cofact :: !(Ops FactionKind)- , coitem :: !(Ops ItemKind)- , coplace :: !(Ops PlaceKind)- , corule :: !(Ops RuleKind)- , costrat :: !(Ops StrategyKind)- , cotile :: !(Ops TileKind)- }---- | The standard ruleset used for level operations.-stdRuleset :: Ops RuleKind -> RuleKind-stdRuleset Ops{ouniqGroup, okind} = okind $ ouniqGroup "standard"--instance Show COps where- show _ = "Game content."---- | 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 location.-newtype Array i c = Array (A.UArray i Word.Word8) deriving Show---- 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---- | 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/Level.hs
@@ -1,202 +0,0 @@--- | Inhabited dungeon levels and the operations to query and change them--- as the game progresses.-module Game.LambdaHack.Level- ( -- * The @Level@ type and its components- ActorDict, InvDict, SmellMap, SecretMap, ItemMap, TileMap, Level(..)- -- * Level update- , updateActorDict, updateInv- , updateSmell, updateIMap, updateLMap, updateLRMap, dropItemsAt- -- * Level query- , at, rememberAt, atI, rememberAtI- , accessible, openable, findLoc, findLocTry- ) where--import Data.Binary-import qualified Data.List as L-import qualified Data.IntMap as IM-import Data.Text (Text)--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.PointXY-import Game.LambdaHack.Point-import Game.LambdaHack.Actor-import Game.LambdaHack.Item-import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.Content.RuleKind-import Game.LambdaHack.Random-import Game.LambdaHack.Tile-import qualified Game.LambdaHack.Feature as F-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Time---- | All actors on the level, indexed by actor identifier.-type ActorDict = IM.IntMap Actor---- | Items carried by actors, indexed by actor identifier.-type InvDict = IM.IntMap [Item]---- | Current smell on map tiles.-type SmellMap = IM.IntMap SmellTime---- | Current secrecy value on map tiles.-type SecretMap = IM.IntMap SecretTime---- | Actual and remembered item lists on map tiles.-type ItemMap = IM.IntMap ([Item], [Item])---- | Tile kinds on the map.-type TileMap = Kind.Array Point TileKind---- | A single, inhabited dungeon level.-data Level = Level- { lactor :: ActorDict -- ^ all actors on the level- , linv :: InvDict -- ^ items belonging to actors- , lxsize :: X -- ^ width of the level- , lysize :: Y -- ^ height of the level- , lsmell :: SmellMap -- ^ smells- , lsecret :: SecretMap -- ^ secrecy values- , litem :: ItemMap -- ^ items on the ground- , lmap :: TileMap -- ^ map tiles- , lrmap :: TileMap -- ^ remembered map tiles- , ldesc :: Text -- ^ level description for the player- , lmeta :: Text -- ^ debug information from cave generation- , lstairs :: (Point, Point) -- ^ destination of the (up, down) stairs- , ltime :: Time -- ^ date of the last activity on the level- , lclear :: Int -- ^ total number of clear tiles- , lseen :: Int -- ^ number of clear tiles already seen- }- deriving Show---- | Update the hero and monster maps.-updateActorDict :: (ActorDict -> ActorDict) -> Level -> Level-updateActorDict f lvl = lvl { lactor = f (lactor lvl) }---- | Update the hero items and monster items maps.-updateInv :: (InvDict -> InvDict) -> Level -> Level-updateInv f lvl = lvl { linv = f (linv lvl) }---- | Update the smell map.-updateSmell :: (SmellMap -> SmellMap) -> Level -> Level-updateSmell f lvl = lvl { lsmell = f (lsmell lvl) }---- | Update the items on the ground map.-updateIMap :: (ItemMap -> ItemMap) -> Level -> Level-updateIMap f lvl = lvl { litem = f (litem lvl) }---- | Update the tile and remembered tile maps.-updateLMap, updateLRMap :: (TileMap -> TileMap) -> Level -> Level-updateLMap f lvl = lvl { lmap = f (lmap lvl) }-updateLRMap f lvl = lvl { lrmap = f (lrmap lvl) }---- Note: do not scatter items around, it's too much work for the player.--- | Place all items on the list at a location on the level.-dropItemsAt :: [Item] -> Point -> Level -> Level-dropItemsAt [] _loc = id-dropItemsAt items loc =- let joinItems = L.foldl' (\ acc i -> snd (joinItem i acc))- adj Nothing = Just (items, [])- adj (Just (i, ri)) = Just (joinItems items i, ri)- in updateIMap (IM.alter adj loc)--instance Binary Level where- put (Level ad ia sx sy ls le li lm lrm ld- lme lstairs ltime lclear lseen) = do- put ad- put ia- put sx- put sy- put ls- put le- put (assert- (IM.null (IM.filter (\ (is1, is2) ->- L.null is1 && L.null is2) li)- `blame` li) li)- put lm- put lrm- put ld- put lme- put lstairs- put ltime- put lclear- put lseen- get = do- ad <- get- ia <- get- sx <- get- sy <- get- ls <- get- le <- get- li <- get- lm <- get- lrm <- get- ld <- get- lme <- get- lstairs <- get- ltime <- get- lclear <- get- lseen <- get- return (Level ad ia sx sy ls le li lm lrm ld- lme lstairs ltime lclear lseen)---- | Query for actual and remembered tile kinds on the map.-at, rememberAt :: Level -> Point -> Kind.Id TileKind-at Level{lmap} p = lmap Kind.! p-rememberAt Level{lrmap} p = lrmap Kind.! p---- Note: representations with 2 maps leads to longer code and slower 'remember'.--- | Query for actual and remembered items on the ground.-atI, rememberAtI :: Level -> Point -> [Item]-atI Level{litem} p = fst $ IM.findWithDefault ([], []) p litem-rememberAtI Level{litem} p = snd $ IM.findWithDefault ([], []) p litem---- | Check whether one location is accessible from another,--- using the formula from the standard ruleset.-accessible :: Kind.COps -> Level -> Point -> Point -> Bool-accessible Kind.COps{ cotile=Kind.Ops{okind=okind}, corule}- lvl@Level{lxsize} sloc tloc =- let check = raccessible $ Kind.stdRuleset corule- src = okind $ lvl `at` sloc- tgt = okind $ lvl `at` tloc- in check lxsize sloc src tloc tgt---- | Check whether the location contains a door of secrecy lower than @k@--- and that can be opened according to the standard ruleset.-openable :: Kind.Ops TileKind -> Level -> SecretTime -> Point -> Bool-openable cops lvl@Level{lsecret} k target =- let tgt = lvl `at` target- in hasFeature cops F.Openable tgt ||- (hasFeature cops F.Hidden tgt &&- lsecret IM.! target <= k)---- | Find a random location on the map satisfying a predicate.-findLoc :: TileMap -> (Point -> Kind.Id TileKind -> Bool) -> Rnd Point-findLoc lmap p =- let search = do- loc <- randomR $ Kind.bounds lmap- let tile = lmap Kind.! loc- if p loc tile- then return loc- else search- in search---- | Try to find a random location on the map satisfying--- the conjunction of the list of predicates.--- If the premitted number of attempts is not enough,--- try again the same number of times without the first predicate,--- then without the first two, etc., until only one predicate remains,--- at which point try as many times, as needed.-findLocTry :: Int -- ^ the number of tries- -> TileMap -- ^ look up in this map- -> [Point -> Kind.Id TileKind -> Bool] -- ^ predicates to satisfy- -> Rnd Point-findLocTry _ lmap [] = findLoc lmap (const (const True))-findLocTry _ lmap [p] = findLoc lmap p-findLocTry numTries lmap l@(_ : tl) = assert (numTries > 0) $- let search 0 = findLocTry numTries lmap tl- search k = do- loc <- randomR $ Kind.bounds lmap- let tile = lmap Kind.! loc- if L.all (\ p -> p loc tile) l- then return loc- else search (k - 1)- in search numTries
− Game/LambdaHack/Misc.hs
@@ -1,25 +0,0 @@--- | Hacks that haven't found their home yet.-module Game.LambdaHack.Misc- ( normalLevelBound, divUp, Freqs, breturn- ) where--import Control.Monad-import Data.Text (Text)---- | Level bounds. TODO: query terminal size instead and scroll view.-normalLevelBound :: (Int, Int)-normalLevelBound = (79, 21)---- | Integer division, rounding up.-divUp :: Int -> Int -> Int-divUp n k = (n + k - 1) `div` k---- | For each group that the kind belongs to, denoted by a @Text@ name--- in the first component of a pair, the second component of a pair shows--- how common the kind is within the group.-type Freqs = [(Text, Int)]---- | @breturn b a = [a | b]@-breturn :: MonadPlus m => Bool -> a -> m a-breturn True a = return a-breturn False _ = mzero
− Game/LambdaHack/Msg.hs
@@ -1,185 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}--- | Game messages displayed on top of the screen for the player to read.-module Game.LambdaHack.Msg- ( makePhrase, makeSentence- , Msg, (<>), (<+>), showT, moreMsg, yesnoMsg, padMsg- , Report, emptyReport, nullReport, singletonReport, addMsg- , splitReport, renderReport- , History, emptyHistory, singletonHistory, addReport, renderHistory- , takeHistory- , Overlay, splitOverlay, stringByLocation- ) where--import qualified Data.List as L-import Data.Char-import Data.Binary-import qualified Data.ByteString.Char8 as BS-import qualified Data.IntMap as IM-import Data.Text (Text)-import qualified Data.Text as T-import Data.Text.Encoding (decodeUtf8, encodeUtf8)-import NLP.Miniutter.English ((<>), (<+>), showT)-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Misc-import Game.LambdaHack.PointXY---- | Re-exported English phrase creation functions, applied to default--- irregular word sets.-makePhrase, makeSentence :: [MU.Part] -> Text-makePhrase = MU.makePhrase MU.defIrregular-makeSentence = MU.makeSentence MU.defIrregular---- | The type of a single message.-type Msg = Text--instance Binary Text where- put = put . encodeUtf8- get = decodeUtf8 `fmap` get---- | The \"press something to see more\" mark.-moreMsg :: Msg-moreMsg = "--more-- "---- | The confirmation request message.-yesnoMsg :: Msg-yesnoMsg = "[yn]"---- | Add spaces at the message end, for display overlayed over the level map.--- Also trims (does not wrap!) too long lines.-padMsg :: X -> Text -> Text-padMsg w xs =- let len = T.length xs- in case compare w len of- LT -> T.snoc (T.take (w - 1) xs) '$'- EQ -> xs- GT -> if T.null xs || T.last xs == ' '- then xs- else T.snoc xs ' '---- | The type of a set of messages to show at the screen at once.-newtype Report = Report [(BS.ByteString, Int)]- deriving Show--instance Binary Report where- put (Report x) = put x- get = fmap Report get---- | Empty set of messages.-emptyReport :: Report-emptyReport = Report []---- | Test if the set of messages is empty.-nullReport :: Report -> Bool-nullReport (Report l) = null l---- | Construct a singleton set of messages.-singletonReport :: Msg -> Report-singletonReport m = addMsg emptyReport m---- | Add message to the end of report.-addMsg :: Report -> Msg -> Report-addMsg r m | T.null m = r-addMsg (Report ((x, n) : xns)) y' | x == y =- Report $ (y, n + 1) : xns- where y = encodeUtf8 y'-addMsg (Report xns) y = Report $ (encodeUtf8 y, 1) : xns---- | Split a messages into chunks that fit in one line.--- We assume the width of the messages line is the same as of level map.-splitReport :: Report -> [Text]-splitReport r =- let w = fst normalLevelBound + 1- in splitText w $ renderReport r---- | Render a report as a (possibly very long) string.-renderReport ::Report -> Text-renderReport (Report []) = T.empty-renderReport (Report (xn : xs)) =- renderReport (Report xs) <+> renderRepetition xn--renderRepetition :: (BS.ByteString, Int) -> Text-renderRepetition (s, 1) = decodeUtf8 s-renderRepetition (s, n) = decodeUtf8 s <> "<x" <> showT n <> ">"---- | Split a string into lines. Avoids ending the line with a character--- other than whitespace or punctuation. Space characters are removed--- from hte start, but never from the end of lines.-splitText :: X -> Text -> [Text]-splitText w xs = splitText' w $ T.dropWhile isSpace 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 (`elem` " .,:;!?") $ T.reverse pre- testPost = T.dropWhile isSpace ppost- in if T.null testPost- then pre : splitText w post- else T.reverse ppost : splitText w (T.reverse ppre <> post)---- | The history of reports.-newtype History = History [Report]- deriving Show--instance Binary History where- put (History x) = put x- get = fmap History get---- | Empty history of reports.-emptyHistory :: History-emptyHistory = History []---- | Construct a singleton history of reports.-singletonHistory :: Report -> History-singletonHistory r = addReport r emptyHistory---- | Render history as many lines of text, wrapping if necessary.-renderHistory :: History -> Overlay-renderHistory (History h) = L.concatMap splitReport h---- | Add a report to history, handling repetitions.-addReport :: Report -> History -> History-addReport (Report []) h = h-addReport m (History []) = History [m]-addReport (Report m) (History (Report h : hs)) =- case (reverse m, h) of- ((s1, n1) : rs, (s2, n2) : hhs) | s1 == s2 ->- let hist = Report ((s2, n1 + n2) : hhs) : hs- in History $ if null rs then hist else Report (reverse rs) : hist- _ -> History $ Report m : Report h : hs---- | Take the given prefix of reports from a history.-takeHistory :: Int -> History -> History-takeHistory k (History h) = History $ take k h---- | A screenful of text lines. When displayed, they are trimmed, not wrapped--- and any lines below the lower screen edge are not visible.-type Overlay = [Text]---- | Split an overlay into overlays that fit on the screen.-splitOverlay :: Y -> Overlay -> [Overlay]-splitOverlay _ [] = [] -- nothing to print over the level area-splitOverlay lysize ls | length ls <= lysize = [ls] -- all fits on one screen-splitOverlay lysize ls = let (pre, post) = splitAt (lysize - 1) ls- in pre : splitOverlay lysize post---- | Returns a function that looks up the characters in the--- string by location. Takes the width and height of the display plus--- the string. Returns also the message to print at the top and bottom.-stringByLocation :: X -> Y -> Overlay- -> (Text, PointXY -> Maybe Char, Maybe Text)-stringByLocation _ _ [] = (T.empty, const Nothing, Nothing)-stringByLocation lxsize lysize (msgTop : ls) =- let over = map (padMsg lxsize) $ take lysize ls- m = IM.fromDistinctAscList $- zip [0..] (L.map (IM.fromList . zip [0..] . T.unpack) over)- msgBottom = case drop lysize ls of- [] -> Nothing- s : _ -> Just s- in (msgTop,- \ (PointXY (x, y)) -> IM.lookup y m >>= \ n -> IM.lookup x n,- msgBottom)
− Game/LambdaHack/Perception.hs
@@ -1,194 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Actors perceiving other actors and the dungeon level.-module Game.LambdaHack.Perception- ( DungeonPerception, Perception- , totalVisible, debugTotalReachable, dungeonPerception- , actorReachesLoc, actorReachesActor, actorSeesActor- ) where--import qualified Data.IntSet as IS-import qualified Data.List as L-import qualified Data.IntMap as IM-import Data.Maybe-import Control.Monad--import Game.LambdaHack.Point-import Game.LambdaHack.State-import Game.LambdaHack.Level-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import Game.LambdaHack.Dungeon-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.FOV-import Game.LambdaHack.Config-import qualified Game.LambdaHack.Tile as Tile-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Content.TileKind--newtype PerceptionReachable = PerceptionReachable- { preachable :: IS.IntSet- }--newtype PerceptionVisible = PerceptionVisible- { pvisible :: IS.IntSet- }---- | The type representing the perception for all levels in the dungeon.-type DungeonPerception = [(LevelId, Perception)]---- | The type representing the perception of all actors on the level.------ Note: Heroes share visibility and only have separate reachability.--- The pplayer field must be void on all levels except where he resides.--- Right now, the field is used only for player-controlled monsters.-data Perception = Perception- { pplayer :: Maybe PerceptionReachable- , pheroes :: IM.IntMap PerceptionReachable- , ptotal :: PerceptionVisible- }---- | The set of tiles visible by at least one hero.-totalVisible :: Perception -> IS.IntSet-totalVisible = pvisible . ptotal---- | For debug only: the set of tiles reachable--- (would be visible if lit) by at least one hero.-debugTotalReachable :: Perception -> IS.IntSet-debugTotalReachable per =- let lpers = maybeToList (pplayer per) ++ IM.elems (pheroes per)- in IS.unions (map preachable lpers)---- | Check whether a location is within the visually reachable area--- of the given actor (disregarding lighting).--- Defaults to false if the actor is not player-controlled (monster or hero).-actorReachesLoc :: ActorId -> Point -> Perception -> Maybe ActorId -> Bool-actorReachesLoc actor loc per pl =- let tryHero = do- hper <- IM.lookup actor (pheroes per)- return $ loc `IS.member` preachable hper- tryPl = do -- the case for a monster under player control- guard $ Just actor == pl- pper <- pplayer per- return $ loc `IS.member` preachable pper- tryAny = tryHero `mplus` tryPl- in fromMaybe False tryAny -- assume not visible, if no perception found---- | Check whether an actor is within the visually reachable area--- of the given actor (disregarding lighting).--- Not quite correct if FOV not symmetric (e.g., @Shadow@).--- Defaults to false if neither actor is player-controlled.-actorReachesActor :: ActorId -> ActorId -> Point -> Point- -> Perception -> Maybe ActorId- -> Bool-actorReachesActor actor1 actor2 loc1 loc2 per pl =- actorReachesLoc actor1 loc2 per pl ||- actorReachesLoc actor2 loc1 per pl---- TODO: When the code for throwing, digital lines and lights is complete.--- make this a special case of ActorSeesActor.--- | Whether a monster can see a hero (@False@ if the target has--- no perceptions, e.g., not a hero or a hero without perception, due--- to being spawned on the same turn by a monster and then seen by another).--- An approximation, to avoid computing FOV for the monster.-monsterSeesHero :: Kind.Ops TileKind -> Perception -> Level- -> ActorId -> ActorId -> Point -> Point -> Bool-monsterSeesHero cotile per lvl _source target sloc tloc =- let rempty = PerceptionReachable IS.empty- reachable@PerceptionReachable{preachable} =- fromMaybe rempty $ IM.lookup target $ pheroes per- in sloc `IS.member` preachable- && isVisible cotile reachable lvl IS.empty tloc---- | Whether an actor can see another. An approximation.-actorSeesActor :: Kind.Ops TileKind -> Perception -> Level- -> ActorId -> ActorId -> Point -> Point -> ActorId -> Bool-actorSeesActor cotile per lvl source target sloc tloc pl =- let heroReaches = actorReachesLoc source tloc per (Just pl)- visByHeroes = tloc `IS.member` totalVisible per- monsterSees = monsterSeesHero cotile per lvl source target sloc tloc- in heroReaches && visByHeroes || monsterSees---- | Calculate the perception of all actors on the level.-dungeonPerception :: Kind.COps -> State -> DungeonPerception-dungeonPerception cops s@State{slid, sdungeon} =- let lvlPer (ln, lvl) = (ln, levelPerception cops s lvl)- in map lvlPer $ currentFirst slid sdungeon---- | Calculate the perception of all actors on the level.-levelPerception :: Kind.COps -> State -> Level -> Perception-levelPerception cops@Kind.COps{cotile}- state@State{ splayer- , sconfig- , sfaction- , sdebug = DebugMode{smarkVision}- }- lvl@Level{lactor} =- let Config{configFovMode} = sconfig- -- Perception for a player-controlled monster on the current level.- mLocPer =- if not (isAHero state splayer) && IM.member splayer lactor- then let m = getPlayerBody state- in Just (bloc m,- computeReachable cops configFovMode smarkVision m lvl)- else Nothing- (mLoc, mPer) = (fmap fst mLocPer, fmap snd mLocPer)- hs = IM.filter (\ m -> bfaction m == sfaction && not (bproj m)) lactor- pers = IM.map (\ h ->- computeReachable cops configFovMode smarkVision h lvl) hs- locs = map bloc $ IM.elems hs- lpers = maybeToList mPer ++ IM.elems pers- reachable = PerceptionReachable $ IS.unions (map preachable lpers)- -- TODO: Instead of giving the monster a light source, alter vision.- playerControlledMonsterLight = maybeToList mLoc- lights = IS.fromList $ playerControlledMonsterLight ++ locs- visible = computeVisible cotile reachable lvl lights- in Perception { pplayer = mPer- , pheroes = pers- , ptotal = visible- }---- | A location can be directly lit by an ambient shine or a weak, portable--- light source, e.g,, carried by a hero. (Only lights of radius 0--- are considered for now and it's assumed they do not reveal hero's position.--- TODO: change this to be radius 1 noctovision and introduce stronger--- light sources that show more but make the hero visible.)--- A location is visible if it's reachable and either directly lit--- or adjacent to one that is at once directly lit and reachable.--- The last condition approximates being--- on the same side of obstacles as the light source.--- The approximation is not exact for multiple heroes, but the discrepancy--- can be attributed to deduction based on combined vague visual hints,--- e.g., if I don't see the reachable light seen by another hero,--- there must be a wall in-between. Stray rays indicate doors,--- moving shadows indicate monsters, etc.-computeVisible :: Kind.Ops TileKind -> PerceptionReachable- -> Level -> IS.IntSet -> PerceptionVisible-computeVisible cops reachable@PerceptionReachable{preachable} lvl lights' =- let lights = IS.intersection lights' preachable -- optimization- isV = isVisible cops reachable lvl lights- in PerceptionVisible $ IS.filter isV preachable--isVisible :: Kind.Ops TileKind -> PerceptionReachable- -> Level -> IS.IntSet -> Point -> Bool-isVisible cotile PerceptionReachable{preachable}- lvl@Level{lxsize, lysize} lights loc0 =- let litDirectly loc = Tile.isLit cotile (lvl `at` loc)- || loc `IS.member` lights- l_and_R loc = litDirectly loc && loc `IS.member` preachable- in litDirectly loc0 || L.any l_and_R (vicinity lxsize lysize loc0)---- | Reachable are all fields on an unblocked path from the hero position.--- The player's own position is considred reachable by him.-computeReachable :: Kind.COps -> FovMode -> Maybe FovMode- -> Actor -> Level -> PerceptionReachable-computeReachable Kind.COps{cotile, coactor=Kind.Ops{okind}}- configFovMode smarkVision actor lvl =- let fovMode m =- if not $ asight $ okind $ bkind m- then Blind- else case smarkVision of- Just fm -> fm- Nothing -> configFovMode- ploc = bloc actor- in PerceptionReachable $- IS.insert ploc $ IS.fromList $ fullscan cotile (fovMode actor) ploc lvl
− Game/LambdaHack/Place.hs
@@ -1,184 +0,0 @@--- | Generation of places from place kinds.-{-# LANGUAGE RankNTypes #-}-module Game.LambdaHack.Place- ( TileMapXY, Place(..), placeValid, buildFence, buildPlace- ) where--import Data.Binary-import qualified Data.Map as M-import qualified Data.List as L-import qualified Data.Set as S-import Data.Text (Text)-import qualified Data.Text as T--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Content.PlaceKind-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Area-import Game.LambdaHack.PointXY-import Game.LambdaHack.Misc-import Game.LambdaHack.Msg()-import Game.LambdaHack.Content.TileKind-import Game.LambdaHack.Random-import Game.LambdaHack.Content.CaveKind---- TODO: use more, rewrite as needed, document each field.--- | The parameters of a place. Most are immutable and set--- at the time when a place is generated.-data Place = Place- { qkind :: !(Kind.Id PlaceKind)- , qarea :: !Area- , qseen :: !Bool- , qlegend :: !Text- , qsolidFence :: !(Kind.Id TileKind)- , qhollowFence :: !(Kind.Id TileKind)- }- deriving Show--instance Binary Place where- put Place{..} = do- put qkind- put qarea- put qseen- put qlegend- put qsolidFence- put qhollowFence- get = do- qkind <- get- qarea <- get- qseen <- get- qlegend <- get- qsolidFence <- get- qhollowFence <- get- return Place{..}---- | 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 @cdefaultTile@.-type TileMapXY = M.Map PointXY (Kind.Id TileKind)---- | For @CAlternate@ tiling, require the place be comprised--- of an even number of whole corners, with exactly one square--- overlap between consecutive coners and no trimming.--- For other tiling methods, check that the area is large enough for tiling--- the corner twice in each direction, with a possible one row/column overlap.-placeValid :: Area -- ^ the area to fill- -> PlaceKind -- ^ the place kind to construct- -> Bool-placeValid r PlaceKind{..} =- let (x0, y0, x1, y1) = expandFence pfence r- dx = x1 - x0 + 1- dy = y1 - y0 + 1- dxcorner = case ptopLeft of [] -> 0 ; l : _ -> T.length l- dycorner = L.length ptopLeft- wholeOverlapped d dcorner = d > 1 && dcorner > 1 &&- (d - 1) `mod` (2 * (dcorner - 1)) == 0- in case pcover of- CAlternate -> wholeOverlapped dx dxcorner &&- wholeOverlapped dy dycorner- _ -> dx >= 2 * dxcorner - 1 &&- dy >= 2 * dycorner - 1---- | Modify available room area according to fence type.-expandFence :: Fence -> Area -> Area-expandFence fence r = case fence of- FWall -> r- FFloor -> expand r (-1)- FNone -> expand r 1---- | Given a few parameters, roll and construct a 'Place' datastructure--- and fill a cave section acccording to it.-buildPlace :: Kind.COps -- ^ the game content- -> CaveKind -- ^ current cave kind- -> Kind.Id TileKind -- ^ fence tile, if fence hollow- -> Int -- ^ current level depth- -> Int -- ^ maximum depth- -> Area -- ^ interior area of the place- -> Rnd (TileMapXY, Place)-buildPlace Kind.COps{ cotile=cotile@Kind.Ops{opick=opick}- , coplace=Kind.Ops{okind=pokind, opick=popick} }- CaveKind{..} qhollowFence ln depth r- = assert (not (trivialArea r) `blame` r) $ do- qsolidFence <- opick cfillerTile (const True)- dark <- chanceDeep ln depth cdarkChance- qkind <- popick (T.pack "rogue") (placeValid r)- let kr = pokind qkind- qlegend = if dark then cdarkLegendTile else clitLegendTile- qseen = False- qarea = expandFence (pfence kr) r- place = assert (validArea qarea `blame` qarea) $- Place{..}- legend <- olegend cotile qlegend- let xlegend = M.insert 'X' qhollowFence legend- return (digPlace place kr xlegend, place)---- | Roll a legend of a place plan: a map from plan symbols to tile kinds.-olegend :: Kind.Ops TileKind -> Text -> Rnd (M.Map Char (Kind.Id TileKind))-olegend Kind.Ops{ofoldrWithKey, opick} group =- let getSymbols _ tk acc =- maybe acc (const $ S.insert (tsymbol tk) acc)- (L.lookup group $ tfreq tk)- symbols = ofoldrWithKey getSymbols S.empty- getLegend s acc = do- m <- acc- tk <- opick group $ (== s) . tsymbol- return $ M.insert s tk m- legend = S.fold getLegend (return M.empty) symbols- in legend---- | Construct a fence around an area, with the given tile kind.-buildFence :: Kind.Id TileKind -> Area -> TileMapXY-buildFence fenceId (x0, y0, x1, y1) =- M.fromList $ [ (PointXY (x, y), fenceId)- | x <- [x0-1, x1+1], y <- [y0..y1] ] ++- [ (PointXY (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- -> M.Map 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 -> M.empty- in M.union (M.map (legend M.!) $ tilePlace qarea kr) fence---- TODO: use Text more instead of [Char]?--- | Create a place by tiling patterns.-tilePlace :: Area -- ^ the area to fill- -> PlaceKind -- ^ the place kind to construct- -> M.Map PointXY Char-tilePlace (x0, y0, x1, y1) pl@PlaceKind{..} =- let dx = x1 - x0 + 1- dy = y1 - y0 + 1- fromX (x, y) = L.map PointXY $ L.zip [x..] (repeat y)- fillInterior :: (forall a. Int -> [a] -> [a]) -> [(PointXY, Char)]- fillInterior f =- let tileInterior (y, row) = L.zip (fromX (x0, y)) $ f dx row- reflected = L.zip [y0..] $ f dy $ map T.unpack ptopLeft- in L.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- interior = case pcover of- CAlternate ->- let tile :: Int -> [a] -> [a]- tile _ [] = assert `failure` pl- tile d pat =- L.take d (L.cycle $ L.init pat ++ L.init (L.reverse pat))- in fillInterior tile- CStretch ->- let stretch :: Int -> [a] -> [a]- stretch _ [] = assert `failure` pl- stretch d pat = tileReflect d (pat ++ L.repeat (L.last pat))- in fillInterior stretch- CReflect ->- let reflect :: Int -> [a] -> [a]- reflect d pat = tileReflect d (L.cycle pat)- in fillInterior reflect- in M.fromList interior
− Game/LambdaHack/Point.hs
@@ -1,102 +0,0 @@--- | Basic operations on 2D points represented as linear offsets.-module Game.LambdaHack.Point- ( Point, toPoint, showPoint- , origin, chessDist, adjacent, vicinity, vicinityCardinal- , inside, displacementXYZ, bla- ) where--import qualified Data.List as L--import Game.LambdaHack.PointXY-import Game.LambdaHack.VectorXY-import Game.LambdaHack.Area-import Game.LambdaHack.Msg-import Game.LambdaHack.Utils.Assert-import Data.Text (Text)---- | The type of locations on the 2D level map, heavily optimized.------ 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 @IntMap@ and cheaper array indexing,--- including cheaper bounds checks.--- We don't define @Point@ as a newtype to avoid the trouble--- with using @EnumMap@ in place of @IntMap@, etc.-type Point = Int---- | Print a point as a tuple of cartesian coordinates.-showPoint :: X -> Point -> Text-showPoint lxsize = showT . fromPoint lxsize---- | Conversion from cartesian coordinates to @Point@.-toPoint :: X -> PointXY -> Point-toPoint lxsize (PointXY (x, y)) =- assert (lxsize > x && x >= 0 && y >= 0 `blame` (lxsize, x, y)) $- x + y * lxsize---- | Conversion from @Point@ to cartesian coordinates.-fromPoint :: X -> Point -> PointXY-fromPoint lxsize loc =- assert (loc >= 0 `blame` (lxsize, loc)) $- PointXY (loc `rem` lxsize, loc `quot` lxsize)---- | The top-left corner location of the level.-origin :: Point-origin = 0---- | The distance between two points in the chessboard metric.-chessDist :: X -> Point -> Point -> Int-chessDist lxsize loc0 loc1- | PointXY (x0, y0) <- fromPoint lxsize loc0- , PointXY (x1, y1) <- fromPoint lxsize loc1 =- chessDistXY $ VectorXY (x1 - x0, 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 locations of a given location.-vicinity :: X -> Y -> Point -> [Point]-vicinity lxsize lysize loc =- map (toPoint lxsize) $- vicinityXY (0, 0, lxsize - 1, lysize - 1) $- fromPoint lxsize loc---- | Returns the 4, or less, surrounding locations in cardinal directions--- from a given location.-vicinityCardinal :: X -> Y -> Point -> [Point]-vicinityCardinal lxsize lysize loc =- map (toPoint lxsize) $- vicinityCardinalXY (0, 0, lxsize - 1, lysize - 1) $- fromPoint lxsize loc---- | Checks that a point belongs to an area.-inside :: X -> Point -> Area -> Bool-inside lxsize loc = insideXY $ fromPoint lxsize loc---- | Calculate the displacement vector from a location to another.-displacementXYZ :: X -> Point -> Point -> VectorXY-displacementXYZ lxsize loc0 loc1- | PointXY (x0, y0) <- fromPoint lxsize loc0- , PointXY (x1, y1) <- fromPoint lxsize loc1 =- VectorXY (x1 - x0, y1 - 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.-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
− Game/LambdaHack/PointXY.hs
@@ -1,60 +0,0 @@--- | Basic cartesian geometry operations on 2D points.-module Game.LambdaHack.PointXY- ( X, Y, PointXY(..), fromTo, sortPointXY, blaXY- ) where--import qualified Data.List as L--import Game.LambdaHack.Utils.Assert---- | 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)---- | 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` ((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)
− Game/LambdaHack/Random.hs
@@ -1,143 +0,0 @@--- | Representation of probabilities and random computations.-module Game.LambdaHack.Random- ( -- * The @Rng@ monad- Rnd- -- * Random operations- , randomR, oneOf, frequency, roll- -- * Rolling dice- , RollDice(..), rollDice, maxDice, minDice, meanDice- -- * Rolling 2D coordinates- , RollDiceXY(..), rollDiceXY- -- * Rolling dependent on depth- , RollDeep, rollDeep, chanceDeep, intToDeep, maxDeep- -- * Fractional chance- , Chance, chance- ) where--import qualified Data.Binary as Binary-import Data.Ratio-import qualified System.Random as R-import Control.Monad-import qualified Data.List as L-import qualified Control.Monad.State as MState--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Utils.Frequency---- | The monad of computations with random generator state.-type Rnd a = MState.State R.StdGen a---- | Get a random object within a range with a uniform distribution.-randomR :: (R.Random a) => (a, a) -> Rnd a-randomR range = MState.state $ R.randomR range---- | Get any element of a list with equal probability.-oneOf :: [a] -> Rnd a-oneOf xs = do- r <- randomR (0, length xs - 1)- return (xs !! r)---- | Gen an element according to a frequency distribution.-frequency :: Show a => Frequency a -> Rnd a-frequency fr = MState.state $ rollFreq fr---- | Roll a single die.-roll :: Int -> Rnd Int-roll x = if x <= 0 then return 0 else randomR (1, x)---- | Dice: 1d7, 3d3, 1d0, etc.--- @RollDice a b@ represents @a@ rolls of @b@-sided die.-data RollDice = RollDice Binary.Word8 Binary.Word8- deriving (Eq, Ord)--instance Show RollDice where- show (RollDice a b) = show a ++ "d" ++ show b--instance Read RollDice where- readsPrec d s =- let (a, db) = L.break (== 'd') s- av = read a- in case db of- 'd' : b -> [ (RollDice av bv, rest) | (bv, rest) <- readsPrec d b ]- _ -> []---- | Roll dice and sum the results.-rollDice :: RollDice -> Rnd Int-rollDice (RollDice a' 1) = return $ fromEnum a' -- optimization-rollDice (RollDice a' b') =- let (a, b) = (fromEnum a', fromEnum b')- in liftM sum (replicateM a (roll b))---- | Maximal value of dice.-maxDice :: RollDice -> Int-maxDice (RollDice a' b') =- let (a, b) = (fromEnum a', fromEnum b')- in a * b---- | Minimal value of dice.-minDice :: RollDice -> Int-minDice (RollDice a' b') =- let (a, b) = (fromEnum a', fromEnum b')- in if b == 0 then 0 else a---- | Mean value of dice.-meanDice :: RollDice -> Rational-meanDice (RollDice a' b') =- let (a, b) = (fromIntegral a', fromIntegral b')- in if b' == 0 then 0 else a * (b + 1) % 2---- | Dice for rolling a pair of integer parameters pertaining to,--- respectively, the X and Y cartesian 2D coordinates.-data RollDiceXY = RollDiceXY (RollDice, RollDice)- deriving Show---- | Roll the two sets of dice.-rollDiceXY :: RollDiceXY -> Rnd (Int, Int)-rollDiceXY (RollDiceXY (xd, yd)) = do- x <- rollDice xd- y <- rollDice yd- return (x, y)---- | Dice for parameters scaled with current level depth.--- 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.-type RollDeep = (RollDice, RollDice)---- | Roll dice scaled with current level depth.--- Note that at the first level, the scaled dice are always ignored.-rollDeep :: Int -> Int -> RollDeep -> Rnd Int-rollDeep n depth (d1, d2) =- assert (n > 0 && n <= depth `blame` (n, depth)) $ do- r1 <- rollDice d1- r2 <- rollDice d2- return $ r1 + ((n - 1) * r2) `div` max 1 (depth - 1)---- | Roll dice scaled with current level depth and return @True@--- if the results if greater than 50.-chanceDeep :: Int -> Int -> RollDeep -> Rnd Bool-chanceDeep n depth deep = do- c <- rollDeep n depth deep- return $ c > 50---- | Generate a @RollDeep@ that always gives a constant integer.-intToDeep :: Int -> RollDeep-intToDeep 0 = (RollDice 0 0, RollDice 0 0)-intToDeep n' = let n = toEnum n'- in if n > maxBound || n < minBound- then assert `failure` n'- else (RollDice n 1, RollDice 0 0)---- | Maximal value of scaled dice.-maxDeep :: RollDeep -> Int-maxDeep (d1, d2) = maxDice d1 + maxDice d2---- | Fractional chance.-type Chance = Rational---- | Give @True@, with probability determined by the fraction.-chance :: Chance -> Rnd Bool-chance r = do- let n = numerator r- d = denominator r- k <- randomR (1, d)- return (k <= n)
− Game/LambdaHack/Running.hs
@@ -1,194 +0,0 @@--- | Running and disturbance.-module Game.LambdaHack.Running- ( run, continueRun- ) where--import Control.Monad.State hiding (State, state)-import qualified Data.List as L-import qualified Data.IntSet as IS--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Action-import Game.LambdaHack.EffectAction-import Game.LambdaHack.Actions-import Game.LambdaHack.Point-import Game.LambdaHack.Vector-import Game.LambdaHack.PointXY-import Game.LambdaHack.Level-import Game.LambdaHack.Msg-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import Game.LambdaHack.Perception-import Game.LambdaHack.State-import qualified Game.LambdaHack.Tile as Tile-import qualified Game.LambdaHack.Kind as Kind-import qualified Game.LambdaHack.Feature as F---- | 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.-run :: (Vector, Int) -> ActionFrame ()-run (dir, dist) = do- cops <- getCOps- pl <- gets splayer- locHere <- gets (bloc . getPlayerBody)- lvl <- gets slevel- targeting <- gets (ctargeting . scursor)- if targeting /= TgtOff- then do- frs <- moveCursor dir 10- -- Mark that unexpectedly it does not take time.- modify (\ s -> s {stakeTime = Just False})- return frs- else do- let accessibleDir loc d = accessible cops lvl loc (loc `shift` d)- -- Do not count distance if we just open a door.- distNew = if accessibleDir locHere dir then dist + 1 else dist- updatePlayerBody (\ p -> p { bdir = Just (dir, distNew) })- -- Attacks and opening doors disallowed when continuing to run.- inFrame $ moveOrAttack False pl dir---- | Player 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 loc 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 loc) (moves lxsize)- in case dirsEnterable of- [] -> assert `failure` (loc, dir)- [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---- | Check for disturbances to running such as newly visible items, monsters.-runDisturbance :: Point -> Int -> Report- -> [Actor] -> [Actor] -> Perception -> Point- -> (F.Feature -> Point -> Bool) -> (Point -> Bool) -> X -> Y- -> (Vector, Int) -> Maybe (Vector, Int)-runDisturbance locLast distLast msg hs ms per locHere- locHasFeature locHasItems lxsize lysize (dirNew, distNew) =- let msgShown = not $ nullReport msg- mslocs = IS.delete locHere $ IS.fromList (L.map bloc ms)- enemySeen = not (IS.null (mslocs `IS.intersection` totalVisible per))- surrLast = locLast : vicinity lxsize lysize locLast- surrHere = locHere : vicinity lxsize lysize locHere- locThere = locHere `shift` dirNew- heroThere = locThere `elem` L.map bloc 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 = [ locHasFeature F.Exit- , locHasItems- ]- -- Here additionally ignore a tile property if you stand on such tile.- standList = [ locHasFeature F.Path- , not . locHasFeature F.Lit- ]- -- 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 = [ locHasFeature F.Lit- , not . locHasFeature F.Path- ]- -- 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 == [locThere]- touchStop fun = touchNew fun /= []- standNew fun = L.filter (\ loc -> locHasFeature F.Walkable loc ||- locHasFeature F.Openable loc)- (touchNew fun)- standExplore fun = not (fun locHere) && standNew fun == [locThere]- standStop fun = not (fun locHere) && standNew fun /= []- firstNew fun = L.all (not . fun) surrLast &&- L.any fun surrHere- firstExplore fun = firstNew fun && fun locThere- 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---- | 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.-continueRun :: (Vector, Int) -> Action ()-continueRun (dirLast, distLast) = do- cops@Kind.COps{cotile} <- getCOps- locHere <- gets (bloc . getPlayerBody)- per <- getPerception- Diary{sreport} <- getDiary- ms <- gets dangerousList- sfaction <- gets sfaction- hs <- gets (factionList [sfaction])- lvl@Level{lxsize, lysize} <- gets slevel- let locHasFeature f loc = Tile.hasFeature cotile f (lvl `at` loc)- locHasItems loc = not $ L.null $ lvl `atI` loc- locLast = if distLast == 0 then locHere else locHere `shift` neg dirLast- tryRunDist (dir, distNew)- | accessibleDir locHere dir =- maybe abort run $- runDisturbance locLast distLast sreport hs ms per locHere- locHasFeature locHasItems 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)- accessibleDir loc dir = accessible cops lvl loc (loc `shift` dir)- openableDir loc dir = Tile.hasFeature cotile F.Openable- (lvl `at` (loc `shift` dir))- dirEnterable loc d = accessibleDir loc d || openableDir loc d- ((), frames) <- case runMode locHere 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- case runMode (locHere `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- when (not $ null frames) abort -- something serious happened, stop running
+ Game/LambdaHack/Server.hs view
@@ -0,0 +1,100 @@+-- | Semantics of server commands.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Server+ ( mainSer+ ) where++import System.Environment (getArgs)++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.ClientCmd+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.ServerCmd+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++-- | The semantics of server commands. The resulting boolean value+-- indicates if the command took some time.+cmdSerSem :: (MonadAtomic m, MonadServer m) => CmdSer -> m Bool+cmdSerSem cmd = case cmd of+ MoveSer aid dir -> moveSer aid dir False+ ExploreSer aid dir -> moveSer aid dir True+ RunSer aid dir -> runSer aid dir+ WaitSer aid -> waitSer aid >> return True+ PickupSer aid i k l -> pickupSer aid i k l >> return True+ DropSer aid iid -> dropSer aid iid >> return True+ ProjectSer aid p eps iid container -> projectSer aid p eps iid container+ ApplySer aid iid container -> applySer aid iid container >> return True+ TriggerSer aid p -> triggerSer aid p+ SetPathSer aid path -> setPathSer aid path >> return True+ GameRestartSer aid t -> gameRestartSer aid t >> return False+ GameExitSer aid -> gameExitSer aid >> return False+ GameSaveSer _ -> gameSaveSer >> return False+ CfgDumpSer aid -> cfgDumpSer aid >> return False++debugArgs :: IO DebugModeSer+debugArgs = do+ args <- getArgs+ let usage =+ [ "Configure server debug options here, gameplay 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"+ , " --debugCli let clients emit their debug messages"+ , " --tryFov m set a Field of View mode, where m can be"+ , " Digital r, r > 0"+ , " Permissive"+ , " Shadow"+ , " Blind"+ ]+ parseArgs [] = defDebugModeSer+ parseArgs ("--knowMap" : rest) =+ (parseArgs rest) {sknowMap = True}+ parseArgs ("--knowEvents" : rest) =+ (parseArgs rest) {sknowEvents = True}+ parseArgs ("--sniffIn" : rest) =+ (parseArgs rest) {sniffIn = True}+ parseArgs ("--sniffOut" : rest) =+ (parseArgs rest) {sniffOut = True}+ parseArgs ("--allClear" : rest) =+ (parseArgs rest) {sallClear = True}+ parseArgs ("--tryFov" : "Digital" : r : rest) | (read r :: Int) > 0 =+ (parseArgs rest) {stryFov = Just $ Digital $ read r}+ parseArgs ("--tryFov" : mode : rest) =+ (parseArgs rest) {stryFov = Just $ read mode}+ parseArgs ("--debugCli" : rest) =+ (parseArgs rest) {sdebugCli = True}+ parseArgs _ = error $ unlines usage+ return $! parseArgs args++-- | Fire up the frontend with the engine fueled by content.+-- The action monad types to be used are determined by the 'exeSer'+-- and 'executorCli' calls. If other functions are used in their place+-- the types are different and so the whole pattern of computation+-- is different. Which of the frontends is run depends on the flags supplied+-- when compiling the engine library.+mainSer :: (MonadAtomic m, MonadConnServer m)+ => Kind.COps+ -> (m () -> IO ())+ -> (Kind.COps+ -> ((FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ())+ -> (FactionId -> ChanServer CmdClientAI -> IO ())+ -> IO ())+ -> IO ())+ -> IO ()+mainSer copsSlow exeSer exeFront = do+ sdebugNxt <- debugArgs+ let cops = speedupCOps False copsSlow+ loopServer = loopSer sdebugNxt cmdSerSem+ exeServer executorUI executorAI = do+ exeSer (loopServer executorUI executorAI cops)+ waitForChildren+ exeFront cops exeServer
+ Game/LambdaHack/Server/Action.hs view
@@ -0,0 +1,382 @@+-- | 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+-- details.+module Game.LambdaHack.Server.Action+ ( -- * Action monads+ MonadServer( getServer, getsServer, putServer, modifyServer )+ , MonadConnServer+ , tryRestore, updateConn, killAllClients, waitForChildren, speedupCOps+ -- * Communication+ , sendUpdateUI, sendQueryUI, sendUpdateAI, sendQueryAI+ -- * Assorted primitives+ , saveGameSer, saveGameBkp, dumpCfg+ , mkConfigRules, restoreScore, revealItems, deduceQuits+ , rndToAction, fovMode, resetFidPerception, getPerFid+ ) where++import Control.Concurrent+import Control.Concurrent.STM (TQueue, atomically, newTQueueIO)+import qualified Control.Concurrent.STM as STM+import Control.Exception (finally)+import Control.Monad+import qualified Control.Monad.State as St+import qualified Data.EnumMap.Strict as EM+import Data.Key (mapWithKeyM, mapWithKeyM_)+import Data.List+import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.IO as T+import System.Directory+import System.IO (stderr)+import System.IO.Unsafe (unsafePerformIO)+import qualified System.Random as R+import System.Time++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 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.ServerCmd+import Game.LambdaHack.Common.State+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Content.RuleKind+import qualified Game.LambdaHack.Frontend as Frontend+import Game.LambdaHack.Server.Action.ActionClass+import qualified Game.LambdaHack.Server.Action.ConfigIO as ConfigIO+import qualified Game.LambdaHack.Server.Action.Save as Save+import Game.LambdaHack.Server.Config+import Game.LambdaHack.Server.Fov+import Game.LambdaHack.Server.State+import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.File++fovMode :: MonadServer m => m FovMode+fovMode = do+ configFovMode <- getsServer (configFovMode . sconfig)+ sdebugSer <- getsServer sdebugSer+ return $ fromMaybe configFovMode $ stryFov sdebugSer++-- | Update the cached perception for the selected level, for a faction.+-- The assumption is the level, and only the level, has changed since+-- the previous perception calculation.+resetFidPerception :: MonadServer m => FactionId -> LevelId -> m ()+resetFidPerception fid lid = do+ cops <- getsState scops+ lvl <- getsLevel lid id+ configFov <- fovMode+ s <- getState+ let per = levelPerception cops s configFov fid lid lvl+ upd = EM.adjust (EM.adjust (const per) lid) fid+ modifyServer $ \ser -> ser {sper = upd (sper ser)}++getPerFid :: MonadServer m => FactionId -> LevelId -> m Perception+getPerFid fid lid = do+ pers <- getsServer sper+ let fper = fromMaybe (assert `failure` (lid, fid)) $ EM.lookup fid pers+ per = fromMaybe (assert `failure` (lid, fid)) $ EM.lookup lid fper+ return $! per++saveGameSer :: MonadServer m => m ()+saveGameSer = do+ s <- getState+ ser <- getServer+ config <- getsServer sconfig+ liftIO $ Save.saveGameSer config s ser++-- | Save a backup of the save game file, in case of crashes.+--+-- See 'Save.saveGameBkp'.+saveGameBkp :: MonadServer m => m ()+saveGameBkp = do+ s <- getState+ ser <- getServer+ config <- getsServer sconfig+ liftIO $ Save.saveGameBkpSer config s ser++-- | Dumps the current game rules configuration to a file.+dumpCfg :: MonadServer m => FilePath -> m ()+dumpCfg fn = do+ config <- getsServer sconfig+ liftIO $ ConfigIO.dump config fn++writeTQueueAI :: MonadConnServer m => CmdClientAI -> TQueue CmdClientAI -> m ()+writeTQueueAI cmd fromServer = do+ debug <- getsServer $ sniffOut . sdebugSer+ when debug $ do+ d <- debugCmdClientAI cmd+ liftIO $ T.hPutStrLn stderr d+ liftIO $ atomically $ STM.writeTQueue fromServer cmd++writeTQueueUI :: MonadConnServer m => CmdClientUI -> TQueue CmdClientUI -> m ()+writeTQueueUI cmd fromServer = do+ debug <- getsServer $ sniffOut . sdebugSer+ when debug $ do+ d <- debugCmdClientUI cmd+ liftIO $ T.hPutStrLn stderr d+ liftIO $ atomically $ STM.writeTQueue fromServer cmd++readTQueue :: MonadConnServer m => TQueue CmdSer -> m CmdSer+readTQueue toServer = do+ cmd <- liftIO $ atomically $ STM.readTQueue toServer+ debug <- getsServer $ sniffIn . sdebugSer+ when debug $ do+ let aid = aidCmdSer cmd+ d <- debugAid aid (showT ("CmdSer", cmd))+ liftIO $ T.hPutStrLn stderr d+ 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 CmdSer+sendQueryAI fid aid = do+ conn <- getsDict $ snd . (EM.! fid)+ writeTQueueAI (CmdQueryAI aid) $ fromServer conn+ readTQueue $ toServer conn++sendUpdateUI :: MonadConnServer m => FactionId -> CmdClientUI -> m ()+sendUpdateUI fid cmd = do+ conn <- getsDict $ snd . fst . (EM.! fid)+ writeTQueueUI cmd $ fromServer conn++sendQueryUI :: MonadConnServer m => FactionId -> ActorId -> m CmdSer+sendQueryUI fid aid = do+ conn <- getsDict $ snd . fst . (EM.! fid)+ writeTQueueUI (CmdQueryUI aid) $ fromServer conn+ readTQueue $ toServer conn++-- | Create 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 -> m (Config, R.StdGen, R.StdGen)+mkConfigRules = liftIO . ConfigIO.mkConfigRules++-- | 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{configScoresFile} = do+ b <- liftIO $ doesFileExist configScoresFile+ if not b+ then return HighScore.empty+ else liftIO $ strictDecodeEOF configScoresFile++-- | Generate a new score, register it and save.+registerScore :: MonadServer m => Status -> Maybe Actor -> FactionId -> m ()+registerScore status mbody fid = do+ assert (maybe True ((fid ==) . bfid) mbody) skip+ factionD <- getsState sfactionD+ let fact = factionD EM.! fid+ assert (isHumanFact fact) skip+ total <- case mbody of+ Just body -> getsState $ snd . calculateTotal body+ Nothing -> case gleader fact of+ Nothing -> return 0+ Just aid -> do+ b <- getsState $ getActorBody aid+ getsState $ snd . calculateTotal b+ config <- getsServer sconfig+ -- Re-read the table in case it's changed by a concurrent game.+ table <- restoreScore config+ time <- getsState stime+ date <- liftIO getClockTime+ let saveScore (ntable, _) =+ liftIO $ encodeEOF (configScoresFile config)+ (ntable :: HighScore.ScoreTable)+ maybe skip saveScore $ HighScore.register table total time status date++revealItems :: (MonadAtomic m, MonadServer m)+ => Maybe FactionId -> Maybe Actor -> m ()+revealItems mfid mbody = do+ dungeon <- getsState sdungeon+ discoS <- getsServer sdisco+ let discover b iid _numPieces = do+ item <- getsState $ getItemBody iid+ let ik = fromJust $ jkind discoS item+ execCmdAtomic $ DiscoverA (blid b) (bpos b) iid ik+ f aid = do+ b <- getsState $ getActorBody aid+ let ourSide = maybe True (== bfid b) mfid+ when (ourSide && Just b /= mbody) $ mapActorItems_ (discover b) b+ mapDungeonActors_ f dungeon+ maybe skip (\b -> mapActorItems_ (discover b) b) mbody++quitF :: (MonadAtomic m, MonadServer m)+ => Maybe Actor -> Status -> FactionId -> m ()+quitF mbody status fid = do+ assert (maybe True ((fid ==) . bfid) mbody) skip+ fact <- getsState $ (EM.! fid) . sfactionD+ let oldSt = gquit fact+ case fmap stOutcome $ oldSt of+ Just Killed -> return () -- Do not overwrite in case+ Just Defeated -> return () -- many things happen in 1 turn.+ Just Conquer -> return ()+ Just Escape -> return ()+ _ -> do+ when (isHumanFact fact) $ do+ revealItems (Just fid) mbody+ registerScore status mbody fid+ execCmdAtomic $ QuitFactionA fid mbody oldSt $ Just status+ modifyServer $ \ser -> ser {squit = True} -- end turn ASAP++-- Send any QuitFactionA actions that can be deduced from their current state.+deduceQuits :: (MonadAtomic m, MonadServer m) => Actor -> Status -> m ()+deduceQuits body Status{stOutcome=Defeated} = assert `failure` body+deduceQuits body Status{stOutcome=Camping} = assert `failure` body+deduceQuits body Status{stOutcome=Restart} = assert `failure` body+deduceQuits body Status{stOutcome=Conquer} = assert `failure` body+deduceQuits body status = do+ cops <- getsState scops+ let fid = bfid body+ mapQuitF statusF fids = mapM_ (quitF Nothing statusF) $ delete fid fids+ quitF (Just body) status fid+ let inGame fact = case fmap stOutcome $ gquit fact of+ Just Killed -> False+ Just Defeated -> False+ Just Restart -> False -- effectively, commits suicide+ _ -> True+ 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+ assocsHuman = filter (isHumanFact . snd) assocsInGame+ case assocsNotSummon of+ _ | null assocsHuman ->+ -- No screensaver mode for now --- all non-human players win.+ mapQuitF status{stOutcome=Conquer} keysInGame+ [] ->+ -- Only summons remain so all win, human or not, allied or not.+ mapQuitF status{stOutcome=Conquer} keysInGame+ (_, fact1) : rest | null assocsSpawn && all (isAllied fact1 . fst) rest ->+ -- Only one allied team remains in a no-spawners game.+ 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.+ let (victors, losers) = partition (flip isAllied fid . snd) assocsInGame+ mapQuitF status{stOutcome=Escape} $ map fst victors+ mapQuitF status{stOutcome=Defeated} $ map fst losers+ _ -> return ()++tryRestore :: MonadServer m+ => Kind.COps -> m (Maybe (State, StateServer))+tryRestore Kind.COps{corule} = do+ let pathsDataFile = rpathsDataFile $ Kind.stdRuleset corule+ -- A throw-away copy of rules config, to be used until the old+ -- version of the config can be read from the savefile.+ (sconfig, _, _) <- mkConfigRules corule+ liftIO $ Save.restoreGameSer sconfig pathsDataFile++-- | Update connections to the new definition of factions.+-- Connect to clients in old or newly spawned threads+-- that read and write directly to the channels.+updateConn :: (MonadAtomic m, MonadConnServer m)+ => (FactionId -> Frontend.ChanFrontend -> ChanServer CmdClientUI+ -> IO ())+ -> (FactionId -> ChanServer CmdClientAI -> IO ())+ -> m ()+updateConn executorUI executorAI = do+ -- Prepare connections based on factions.+ oldD <- getDict+ let mkChanServer :: IO (ChanServer c)+ mkChanServer = do+ fromServer <- newTQueueIO+ toServer <- newTQueueIO+ return ChanServer{..}+ mkChanFrontend :: IO Frontend.ChanFrontend+ mkChanFrontend = newTQueueIO+ addConn fid _ = case EM.lookup fid oldD of+ Just conns -> return conns -- share old conns and threads+ Nothing -> do+ connF <- mkChanFrontend+ connS <- mkChanServer+ connAI <- mkChanServer+ return ((connF, connS), connAI)+ factionD <- getsState sfactionD+ d <- liftIO $ mapWithKeyM addConn factionD+ let newD = d `EM.union` oldD -- never kill old clients+ putDict newD+ -- Spawn and kill client threads.+ let toSpawn = newD EM.\\ oldD+ fdict fid = ( fst $ fst+ $ fromMaybe (assert `failure` fid)+ $ EM.lookup fid newD+ , maybe T.empty gname -- a faction can go inactive+ $ EM.lookup fid factionD+ )+ fromM = Frontend.fromMulti Frontend.connMulti+ liftIO $ void $ takeMVar fromM -- stop Frontend+ let forkUI fid (connF, connS) = void $ forkChild $ executorUI fid connF connS+ forkAI fid connS = void $ forkChild $ executorAI fid connS+ forkClient fid (connUI, connAI) = do+ -- This is fragile: when a connection is reused, clients are not+ -- respawned, even if AI or UI usage changes (it does not, currently,+ -- thanks to faction numbering, etc.).+ -- TODO: perhaps spawn both AI and UI clients always, but then+ -- the UI client should not display a welcome message, until+ -- the server tells it to.+ when (isHumanFact $ factionD EM.! fid) $ forkUI fid connUI+ when (usesAIFact $ factionD EM.! fid) $ forkAI fid connAI+ liftIO $ mapWithKeyM_ forkClient toSpawn+ nH <- nHumans+ liftIO $ putMVar fromM (nH, fdict) -- restart Frontend++killAllClients :: (MonadAtomic m, MonadConnServer m) => m ()+killAllClients = do+ d <- getDict+ let sendKill fid _ = do+ sendUpdateUI fid $ CmdAtomicUI $ KillExitA fid+ sendUpdateAI fid $ CmdAtomicAI $ KillExitA fid+ mapWithKeyM_ sendKill d++-- Swiped from http://www.haskell.org/ghc/docs/latest/html/libraries/base-4.6.0.0/Control-Concurrent.html+children :: MVar [MVar ()]+{-# NOINLINE children #-}+children = unsafePerformIO (newMVar [])++waitForChildren :: IO ()+waitForChildren = do+ cs <- takeMVar children+ case cs of+ [] -> return ()+ m : ms -> do+ putMVar children ms+ takeMVar m+ waitForChildren++forkChild :: IO () -> IO ThreadId+forkChild io = do+ mvar <- newEmptyMVar+ childs <- takeMVar children+ putMVar children (mvar : childs)+ forkIO (io `finally` putMVar mvar ())+-- 7.6 forkFinally io (\_ -> putMVar mvar ())++-- | Compute and insert auxiliary optimized components into game content,+-- to be used in time-critical sections of the code.+speedupCOps :: Bool -> Kind.COps -> Kind.COps+speedupCOps allClear copsSlow@Kind.COps{cotile=tile} =+ let ospeedup = Tile.speedup allClear tile+ cotile = tile {Kind.ospeedup = ospeedup}+ in copsSlow {Kind.cotile = cotile}++-- | Invoke pseudo-random computation with the generator kept in the state.+rndToAction :: MonadServer m => Rnd a -> m a+rndToAction r = do+ g <- getsServer srandom+ let (a, ng) = St.runState r g+ modifyServer $ \ser -> ser {srandom = ng}+ return a
+ Game/LambdaHack/Server/Action/ActionClass.hs view
@@ -0,0 +1,23 @@+-- | Basic type classes for server game actions.+-- This module should not be imported anywhere except in 'Action'+-- and 'TypeAction'.+module Game.LambdaHack.Server.Action.ActionClass where++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.ClientCmd+import Game.LambdaHack.Server.State++class MonadActionRO m => MonadServer m where+ getServer :: m StateServer+ getsServer :: (StateServer -> a) -> m a+ modifyServer :: (StateServer -> StateServer) -> m ()+ putServer :: StateServer -> m ()+ -- We do not provide a MonadIO instance, so that outside of Action/+ -- nobody can subvert the action monads by invoking arbitrary IO.+ liftIO :: IO a -> m a++class MonadServer m => MonadConnServer m where+ getDict :: m ConnServerDict+ getsDict :: (ConnServerDict -> a) -> m a+ modifyDict :: (ConnServerDict -> ConnServerDict) -> m ()+ putDict :: ConnServerDict -> m ()
+ Game/LambdaHack/Server/Action/ActionType.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+-- | 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.Server.Action.ActionType+ ( ActionSer, executorSer+ ) where++import qualified Control.Monad.IO.Class as IO+import Control.Monad.Trans.State.Strict hiding (State)+import qualified Data.EnumMap.Strict as EM++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.ClientCmd+import Game.LambdaHack.Common.State+import Game.LambdaHack.Server.Action.ActionClass+import Game.LambdaHack.Server.State++data SerState = SerState+ { serState :: !State -- ^ current global state+ , serServer :: !StateServer -- ^ current server state+ , serDict :: !ConnServerDict -- ^ client-server connection information+ }++-- | Server state transformation monad.+newtype ActionSer a = ActionSer {runActionSer :: StateT SerState IO a}+ deriving (Monad, Functor)++instance MonadActionRO ActionSer where+ getState = ActionSer $ gets serState+ 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}++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}+ liftIO = ActionSer . IO.liftIO++instance MonadConnServer ActionSer where+ getDict = ActionSer $ gets serDict+ getsDict f = ActionSer $ gets $ f . serDict+ modifyDict f =+ ActionSer $ modify $ \serS -> serS {serDict = f $ serDict serS}+ putDict s =+ ActionSer $ modify $ \serS -> serS {serDict = s}++-- | Run an action in the @IO@ monad, with undefined state.+executorSer :: ActionSer () -> IO ()+executorSer m = evalStateT (runActionSer m)+ SerState { serState = emptyState+ , serServer = emptyStateServer+ , serDict = EM.empty+ }
+ Game/LambdaHack/Server/Action/ConfigIO.hs view
@@ -0,0 +1,169 @@+-- TODO: factor out parts common with Client.ConfigIO+-- | Personal game configuration file support.+module Game.LambdaHack.Server.Action.ConfigIO+ ( mkConfigRules, dump+ ) where++import Control.Arrow (first, (***))+import Control.DeepSeq+import qualified Data.Char as Char+import qualified Data.ConfigFile as CF+import qualified Data.EnumMap.Strict as EM+import Data.List+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import System.Directory+import System.Environment+import System.FilePath+import qualified System.Random as R++import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Server.Config+import Game.LambdaHack.Utils.Assert++-- TODO: Refactor the client and server ConfigIO.hs, after+-- https://github.com/kosmikus/LambdaHack/issues/45.++overrideCP :: CP -> FilePath -> IO CP+overrideCP cp@(CP defCF) cfile = do+ b <- doesFileExist cfile+ if not b+ then return cp+ else do+ c <- CF.readfile defCF cfile+ return $ toCP $ forceEither c++-- | Read a player configuration file and use it to override+-- options from a default config. Currently we can't unset options,+-- only override. The default config, passed in argument @configDefault@,+-- is expected to come from a default configuration file included via CPP.+-- The player configuration comes from file @cfile@.+mkConfig :: String -> FilePath -> IO CP+mkConfig configDefault cfile = do+ let delComment = map (drop 2) $ init . drop 3 $ lines configDefault+ unConfig = unlines delComment+ -- Evaluate, to catch config errors ASAP.+ !defCF = 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: " ++ s ++ "." ++ o+ else CP $ forceEither $ CF.set conf s o v++-- | Gets a random generator from the config or,+-- if not present, generates one and updates the config with it.+getSetGen :: CP -- ^ config+ -> String -- ^ name of the generator+ -> IO (R.StdGen, CP)+getSetGen config option =+ case getOption config "engine" option of+ Just sg -> return (read sg, config)+ Nothing -> do+ -- Pick the randomly chosen generator from the IO monad+ -- and record it in the config for debugging (can be 'D'umped).+ g <- R.newStdGen+ let gs = show g+ c = set config "engine" option gs+ return (g, c)++-- | The content of the configuration file. It's parsed+-- in a case sensitive way (unlike by default in ConfigFile).+newtype CP = CP CF.ConfigParser++instance Show CP where+ show (CP conf) = show $ CF.to_string conf++-- | Switches all names to case sensitive (unlike by default in+-- the "ConfigFile" library) and wraps in the constructor.+toCP :: CF.ConfigParser -> CP+toCP cf = CP $ cf {CF.optionxform = id}++-- | In case of corruption, just fail.+forceEither :: Show a => Either a b -> b+forceEither (Left a) = assert `failure` a+forceEither (Right b) = b++-- | A simplified access to an option in a given section,+-- with simple error reporting (no internal errors are caught nor hidden).+-- If there is no such option, gives Nothing.+getOption :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> Maybe a+getOption (CP conf) s o =+ if CF.has_option conf s o+ then Just $ forceEither $ CF.get conf s o+ else Nothing++-- | Simplified access to an option in a given section.+-- Fails if the option is not present.+get :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> a+get (CP conf) s o =+ if CF.has_option conf s o+ then forceEither $ CF.get conf s o+ else assert `failure` "Unknown config option: " ++ s ++ "." ++ o++-- | An association list corresponding to a section. Fails if no such section.+getItems :: CP -> CF.SectionSpec -> [(String, String)]+getItems (CP conf) s =+ if CF.has_section conf s+ then forceEither $ CF.items conf s+ else assert `failure` "Unknown config section: " ++ s++parseConfigRules :: FilePath -> CP -> Config+parseConfigRules dataDir cp =+ let configSelfString = let CP conf = cp in CF.to_string conf+ configCaves =+ let section = getItems cp "caves"+ readCaves = EM.fromList . map (toEnum *** first T.pack)+ in M.fromList $ map (T.pack *** (readCaves . read)) section+ configFirstDeathEnds = get cp "engine" "firstDeathEnds"+ configFovMode = get cp "engine" "fovMode"+ configSaveBkpClips = get cp "engine" "saveBkpClips"+ configAppDataDir = dataDir+ configScoresFile = dataDir </> get cp "file" "scoresFile"+ configRulesCfgFile = dataDir </> "config.rules"+ configHeroNames =+ let toNumber (ident, name) =+ case stripPrefix "HeroName_" ident of+ Just n -> (read n, T.pack name)+ Nothing -> assert `failure` ("wrong hero name id " ++ ident)+ section = getItems cp "heroName"+ in map toNumber section+ configPlayers =+ let section = getItems cp "players"+ in M.fromList $ map (T.pack *** read) section+ configScenario =+ let section = getItems cp "scenario"+ in M.fromList $ map (T.pack *** read) section+ in Config{..}++-- | Read and parse rules config file and supplement it with random seeds.+mkConfigRules :: Kind.Ops RuleKind -> IO (Config, R.StdGen, R.StdGen)+mkConfigRules corule = do+ let cpRulesDefault = rcfgRulesDefault $ Kind.stdRuleset corule+ appData <- appDataDir+ cpRules <- mkConfig cpRulesDefault $ appData </> "config.rules.ini"+ (dungeonGen, cp2) <- getSetGen cpRules "dungeonRandomGenerator"+ (startingGen, cp3) <- getSetGen cp2 "startingRandomGenerator"+ let conf = parseConfigRules appData cp3+ -- Catch syntax errors ASAP.+ !res = deepseq conf (conf, dungeonGen, startingGen)+ return res
+ Game/LambdaHack/Server/Action/Save.hs view
@@ -0,0 +1,94 @@+-- | Saving and restoring server game state.+module Game.LambdaHack.Server.Action.Save+ ( saveGameBkpSer, saveGameSer, restoreGameSer+ ) where++import Control.Concurrent+import qualified Control.Exception as Ex hiding (handle)+import Control.Monad+import System.Directory+import System.FilePath+import System.IO+import System.IO.Unsafe (unsafePerformIO)++import Game.LambdaHack.Common.State+import Game.LambdaHack.Server.Config+import Game.LambdaHack.Server.State+import Game.LambdaHack.Utils.File++-- TODO: Refactor the client and server Save.hs, after+-- https://github.com/kosmikus/LambdaHack/issues/37.++saveLock :: MVar ()+{-# NOINLINE saveLock #-}+saveLock = unsafePerformIO newEmptyMVar++-- | Save game to the backup savefile, in case of crashes.+-- This is only a backup, so no problem is the game is shut down+-- before saving finishes, so we don't wait on the mvar. However,+-- if a previous save is already in progress, we skip this save.+saveGameBkpSer :: Config -> State -> StateServer -> IO ()+saveGameBkpSer Config{configAppDataDir} s ser = do+ b <- tryPutMVar saveLock ()+ when b $+ void $ forkIO $ do+ let saveFile = configAppDataDir </> "server.sav"+ saveFileBkp = saveFile <.> ".bkp"+ encodeEOF saveFile (s, ser)+ renameFile saveFile saveFileBkp+ takeMVar saveLock++-- | Save a simple serialized version of the current state.+-- Protected by a lock to avoid corrupting the file.+saveGameSer :: Config -> State -> StateServer -> IO ()+saveGameSer Config{configAppDataDir} s ser = do+ putMVar saveLock ()+ let saveFile = configAppDataDir </> "server.sav"+ encodeEOF saveFile (s, ser)+ takeMVar saveLock++-- | Restore a saved game, if it exists. Initialize directory structure+-- and cope over data files, if needed.+restoreGameSer :: Config -> (FilePath -> IO FilePath)+ -> IO (Maybe (State, StateServer))+restoreGameSer Config{ configAppDataDir+ , configRulesCfgFile+ , configScoresFile }+ pathsDataFile = do+ -- Create user data directory and copy files, if not already there.+ tryCreateDir configAppDataDir+ tryCopyDataFiles pathsDataFile+ [ (configRulesCfgFile <.> ".default", configRulesCfgFile <.> ".ini")+ , (configScoresFile, configScoresFile) ]+ let saveFile = configAppDataDir </> "server.sav"+ saveFileBkp = saveFile <.> ".bkp"+ sb <- doesFileExist saveFile+ bb <- doesFileExist saveFileBkp+ when sb $ renameFile saveFile saveFileBkp+ -- If the savefile exists but we get IO or decoding errors, we show them,+ -- back up the savefile, move it out of the way and start a new game.+ -- If the savefile was randomly corrupted or made read-only,+ -- that should solve the problem. Serious IO problems (e.g. failure+ -- to create a user data directory) terminate the program with an exception.+ res <- Ex.try $+ if sb+ then do+ (s, ser) <- strictDecodeEOF saveFileBkp+ return $ Just (s, ser)+ else+ if bb+ then do+ (s, ser) <- strictDecodeEOF saveFileBkp+ let msg = "No server savefile found. "+ ++ "Restoring from a backup savefile."+ hPutStrLn stderr msg+ return $ Just (s, ser)+ else return Nothing+ let handler :: Ex.SomeException -> IO (Maybe (State, StateServer))+ handler e = do+ let msg = "Starting a new game, because server restore failed. "+ ++ "The error message is: "+ ++ (unwords . lines) (show e)+ hPutStrLn stderr msg+ return Nothing+ either handler return res
+ Game/LambdaHack/Server/AtomicSemSer.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Sending atomic commands to clients and executing them on the server.+-- See+-- <https://github.com/kosmikus/LambdaHack/wiki/Client-server-architecture>.+module Game.LambdaHack.Server.AtomicSemSer+ ( atomicSendSem+ ) where++import Control.Monad+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.Key (mapWithKeyM_)+import Data.Maybe++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.AtomicPos+import Game.LambdaHack.Common.AtomicSem+import Game.LambdaHack.Common.ClientCmd+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.Perception+import Game.LambdaHack.Common.State+import Game.LambdaHack.Server.Action+import Game.LambdaHack.Server.State+import Game.LambdaHack.Utils.Assert++storeUndo :: MonadServer m => Atomic -> m ()+storeUndo _atomic =+ maybe skip (\a -> modifyServer $ \ser -> ser {sundo = a : sundo ser})+ $ Nothing -- TODO: undoAtomic atomic++atomicServerSem :: (MonadAction m, MonadServer m)+ => PosAtomic -> Atomic -> m ()+atomicServerSem posAtomic atomic =+ when (seenAtomicSer posAtomic) $ do+ storeUndo atomic+ case atomic of+ CmdAtomic cmd -> cmdAtomicSem cmd+ SfxAtomic _ -> return ()++-- | Send an atomic action to all clients that can see it.+atomicSendSem :: (MonadAction m, MonadConnServer m) => Atomic -> m ()+atomicSendSem atomic = do+ -- Gather data from the old state.+ sOld <- getState+ factionD <- getsState sfactionD+ persOld <- getsServer sper+ (ps, resets, atomicBroken, psBroken) <-+ case atomic of+ CmdAtomic cmd -> do+ ps <- posCmdAtomic cmd+ resets <- resetsFovAtomic cmd+ atomicBroken <- breakCmdAtomic cmd+ psBroken <- mapM posCmdAtomic atomicBroken+ return (ps, resets, atomicBroken, psBroken)+ SfxAtomic sfx -> do+ ps <- posSfxAtomic sfx+ return (ps, Just [], [], [])+ let atomicPsBroken = zip atomicBroken psBroken+ -- TODO: assert also that the sum of psBroken is equal to ps+ -- TODO: with deep equality these assertions can be expensive. Optimize.+ assert (case ps of+ PosSight{} -> True+ PosFidAndSight{} -> True+ _ -> resets == Just []+ && (null atomicBroken+ || fmap CmdAtomic atomicBroken == [atomic])) skip+ -- Perform the action on the server.+ atomicServerSem ps atomic+ -- Send some actions to the clients, one faction at a time.+ knowEvents <- getsServer $ sknowEvents . sdebugSer+ let sendUI fid cmdUI =+ when (isHumanFact $ factionD EM.! fid) $ sendUpdateUI fid cmdUI+ sendAI fid cmdAI =+ when (usesAIFact $ factionD EM.! fid) $ sendUpdateAI fid cmdAI+ sendA fid cmd = do+ sendUI fid $ CmdAtomicUI cmd+ sendAI fid $ CmdAtomicAI cmd+ sendUpdate fid (CmdAtomic cmd) = sendA fid cmd+ sendUpdate fid (SfxAtomic sfx) = sendUI fid $ SfxAtomicUI sfx+ breakSend fid perNew = do+ let send2 (atomic2, ps2) =+ if seenAtomicCli knowEvents fid perNew ps2+ then sendUpdate fid $ CmdAtomic atomic2+ else when (loudCmdAtomic fid atomic2) $+ sendUpdate fid+ $ SfxAtomic $ MsgAllD "You hear some noises."+ mapM_ send2 atomicPsBroken+ anySend fid perOld perNew = do+ let startSeen = seenAtomicCli knowEvents fid perOld ps+ endSeen = seenAtomicCli knowEvents fid perNew ps+ if startSeen && endSeen+ then sendUpdate fid atomic+ else breakSend fid perNew+ posLevel fid lid = do+ let perOld = persOld EM.! fid EM.! lid+ resetsFid = maybe True (fid `elem`) resets+ if resetsFid then do+ 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+ then anySend fid perOld perOld+ else do+ sendA fid $ PerceptionA lid outPA inPA+ unless knowEvents $ do -- inconsistencies would quickly manifest+ let remember = atomicRemember lid inPer sOld+ seenNew = seenAtomicCli False fid perNew+ seenOld = seenAtomicCli False fid perOld+ -- TODO: these assertions are probably expensive+ psRem <- mapM posCmdAtomic remember+ -- Verify that we remember only currently seen things.+ assert (allB seenNew psRem) skip+ -- Verify that we remember only new things.+ assert (allB (not . seenOld) psRem) skip+ mapM_ (sendA fid) remember+ anySend fid perOld perNew+ else anySend fid perOld perOld+ send fid = case ps of+ PosSight lid _ -> posLevel fid lid+ PosFidAndSight _ lid _ -> posLevel fid lid+ -- In the following cases, from the assertion above,+ -- @resets@ is false here and broken atomic has the same ps.+ PosSmell lid _ -> do+ let perOld = persOld EM.! fid EM.! lid+ anySend fid perOld perOld+ PosFid fid2 -> when (fid == fid2) $ sendUpdate fid atomic+ PosFidAndSer fid2 -> when (fid == fid2) $ sendUpdate fid atomic+ PosSer -> return ()+ PosAll -> sendUpdate fid atomic+ PosNone -> assert `failure` (atomic, fid)+ mapWithKeyM_ (\fid _ -> send fid) factionD++atomicRemember :: LevelId -> Perception -> State -> [CmdAtomic]+atomicRemember lid inPer s =+ -- No @LoseItemA@ is sent for items that became out of sight.+ -- The client will create these atomic actions based on @outPer@,+ -- if required. Any client that remembers out of sight items, OTOH,+ -- will create atomic actions that forget remembered items+ -- that are revealed not to be there any more (no @SpotItemA@ for them).+ -- Similarly no @LoseActorA@, @LoseTileA@ nor @LoseSmellA@.+ 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)+ inActor = map fActor inPrio+ -- Items.+ pMaybe p = maybe Nothing (\x -> Just (p, x))+ inFloor = mapMaybe (\p -> pMaybe p $ EM.lookup p (lfloor lvl)) inFov+ fItem p (iid, k) = SpotItemA iid (getItemBody iid s) k (CFloor lid p)+ fBag (p, bag) = map (fItem p) $ EM.assocs bag+ inItem = concatMap fBag inFloor+ -- Tiles.+ cotile = Kind.cotile (scops s)+ inTileMap = map (\p -> (p, hideTile cotile p lvl)) inFov+ atomicTile = if null inTileMap then [] else [SpotTileA lid inTileMap]+ -- TODO: somehow also use this+ -- bonus = case strongestSearch itemAssocs of+ -- Just (k, _) -> k + 1+ -- Nothing -> 1+ -- TODO: add 'search' that rescans FOV, perhaps with a bonus+ -- TODO: add 'explore' that tells a tile is a hidden door, etc.+ -- TODO: when explored tile is boring, display tips, monster scratches,+ -- old inscriptions and other flavour, as in UnAngband+ -- 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+ atomicSmell = if null inSm then [] else [SpotSmellA lid inSm]+ in inItem ++ inActor ++ atomicTile ++ atomicSmell
+ Game/LambdaHack/Server/Config.hs view
@@ -0,0 +1,130 @@+-- | Personal game configuration file type definitions.+module Game.LambdaHack.Server.Config+ ( Config(..), Caves, Players(..), Player(..), Scenario(..)+ ) where++import Control.DeepSeq+import Data.Binary+import qualified Data.EnumMap.Strict as EM+import Data.Map.Strict (Map)+import Data.Text (Text)++import Game.LambdaHack.Common.Level+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+ -- caves+ , configCaves :: !(Map Text Caves)+ -- engine+ , configFirstDeathEnds :: !Bool+ , configFovMode :: !FovMode+ , configSaveBkpClips :: !Int+ -- files+ , configAppDataDir :: !FilePath+ , configScoresFile :: !FilePath+ , configRulesCfgFile :: !FilePath+ -- heroNames+ , configHeroNames :: ![(Int, Text)]+ -- players+ , configPlayers :: !(Map Text Players)+ -- scenario+ , configScenario :: !(Map Text Scenario)+ }+ deriving Show++type Caves = EM.EnumMap LevelId (Text, Bool)++data Players = Players+ { playersHuman :: [Player]+ , playersComputer :: [Player]+ , playersEnemy :: [(Text, Text)]+ , playersAlly :: [(Text, Text)]+ }+ deriving (Show, Read)++data Player = Player+ { playerName :: Text+ , playerKind :: Text+ , playerInitial :: Int+ , playerEntry :: LevelId+ }+ deriving (Show, Read)++instance NFData Player++instance Binary Player where+ put Player{..} = do+ put playerName+ put playerKind+ put playerInitial+ put playerEntry+ get = do+ playerName <- get+ playerKind <- get+ playerInitial <- get+ playerEntry <- get+ return Player{..}++instance NFData Players++instance Binary Players where+ put Players{..} = do+ put playersHuman+ put playersComputer+ put playersEnemy+ put playersAlly+ get = do+ playersHuman <- get+ playersComputer <- get+ playersEnemy <- get+ playersAlly <- get+ return Players{..}++data Scenario = Scenario+ { scenarioPlayers :: Text+ , scenarioDungeon :: Text+ }+ deriving (Show, Read)++instance NFData Scenario++instance Binary Scenario where+ put Scenario{..} = do+ put scenarioPlayers+ put scenarioDungeon+ get = do+ scenarioPlayers <- get+ scenarioDungeon <- get+ return Scenario{..}++instance NFData Config++instance Binary Config where+ put Config{..} = do+ put configSelfString+ put configCaves+ put configFirstDeathEnds+ put configFovMode+ put configSaveBkpClips+ put configAppDataDir+ put configScoresFile+ put configRulesCfgFile+ put configHeroNames+ put configPlayers+ put configScenario+ get = do+ configSelfString <- get+ configCaves <- get+ configFirstDeathEnds <- get+ configFovMode <- get+ configSaveBkpClips <- get+ configAppDataDir <- get+ configScoresFile <- get+ configRulesCfgFile <- get+ configHeroNames <- get+ configPlayers <- get+ configScenario <- get+ return Config{..}
+ Game/LambdaHack/Server/DungeonGen.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE OverloadedStrings #-}+-- | The main dungeon generation routine.+module Game.LambdaHack.Server.DungeonGen+ ( FreshDungeon(..), dungeonGen+ ) where++import Control.Arrow (first)+import Control.Monad+import qualified Control.Monad.State as St+import qualified Data.EnumMap.Strict as EM+import Data.List+import Data.Maybe+import qualified System.Random as R++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.Level+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.CaveKind+import Game.LambdaHack.Content.TileKind+import Game.LambdaHack.Server.Config+import Game.LambdaHack.Server.DungeonGen.Cave hiding (TileMapXY)+import Game.LambdaHack.Server.DungeonGen.Place+import Game.LambdaHack.Utils.Assert++convertTileMaps :: Rnd (Kind.Id TileKind) -> Int -> Int -> TileMapXY+ -> 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++placeStairs :: Kind.Ops TileKind -> TileMap -> CaveKind -> [Place]+ -> Rnd ( Point, Kind.Id TileKind+ , Point, Kind.Id TileKind+ , Point, Kind.Id TileKind )+placeStairs cotile@Kind.Ops{opick} cmap CaveKind{..} dplaces = do+ su <- findPos cmap (const (Tile.hasFeature cotile F.CanActor))+ sd <- findPosTry 1000 cmap+ [ \ l _ -> chessDist cxsize su l >= cminStairDist+ , \ l _ -> chessDist cxsize su l >= cminStairDist `div` 2+ , \ l t -> l /= su && Tile.hasFeature cotile F.CanActor t+ ]+ sq <- findPos cmap (const (Tile.hasFeature cotile F.CanActor))+ let fitArea pos = inside cxsize pos . qarea+ findLegend pos =+ maybe clitLegendTile qlegend $ find (fitArea pos) dplaces+ upEscape <-+ opick (findLegend su) $ Tile.kindHasFeature $ F.Cause Effect.Escape+ upId <- opick (findLegend su) $ Tile.kindHasFeature F.Ascendable+ downId <- opick (findLegend sd) $ Tile.kindHasFeature F.Descendable+ return (sq, upEscape, su, upId, sd, downId)++-- | Create a level from a cave, from a cave kind.+buildLevel :: Kind.COps -> Cave -> Int -> Int -> Int -> Bool -> Rnd Level+buildLevel Kind.COps{ cotile=cotile@Kind.Ops{opick}+ , cocave=Kind.Ops{okind} }+ Cave{..} ldepth minD maxD escapeFeature = do+ let kc@CaveKind{..} = okind dkind+ cmap <- convertTileMaps (opick cdefTile (const True)) cxsize cysize dmap+ (sq, upEscape, su, upId, sd, downId) <- placeStairs cotile cmap kc dplaces+ litemNum <- rollDice citemNum+ secret <- random+ let stairs = (if ldepth == minD then [] else [(su, upId)])+ ++ (if ldepth == maxD then [] else [(sd, downId)])+ ++ (if not escapeFeature then [] else [(sq, upEscape)])+ ltile = cmap Kind.// stairs+ f !n !tk | Tile.isExplorable cotile tk = n + 1+ | otherwise = n+ lclear = Kind.foldlArray f 0 ltile+ -- TODO: split this into Level.defaultLevel+ level = Level+ { ldepth+ , lprio = EM.empty+ , lfloor = EM.empty+ , ltile+ , lxsize = cxsize+ , lysize = cysize+ , lsmell = EM.empty+ , ldesc = cname+ , lstair = (su, sd)+ , lseen = 0+ , lclear+ , ltime = timeTurn+ , litemNum+ , lsecret = secret+ , lhidden = chidden+ }+ return level++findGenerator :: Kind.COps -> Caves -> LevelId -> LevelId -> LevelId+ -> Rnd Level+findGenerator cops caves ldepth minD maxD = do+ let Kind.COps{cocave=Kind.Ops{opick}} = cops+ (genName, escapeFeature) =+ fromMaybe ("dng", False) $ EM.lookup ldepth caves+ ci <- opick genName (const True)+ let maxDepth = if minD == maxD then 10 else fromEnum maxD+ cave <- buildCave cops (fromEnum ldepth) maxDepth ci+ buildLevel cops cave+ (fromEnum ldepth) (fromEnum minD) (fromEnum maxD)+ escapeFeature++-- | Freshly generated and not yet populated dungeon.+data FreshDungeon = FreshDungeon+ { freshDungeon :: !Dungeon -- ^ maps for all levels+ , freshDepth :: !Int -- ^ dungeon depth (can be different than size)+ }++-- | Generate the dungeon for a new game.+dungeonGen :: Kind.COps -> Caves -> Rnd FreshDungeon+dungeonGen cops caves = do+ let (minD, maxD) =+ case (EM.minViewWithKey caves, EM.maxViewWithKey caves) of+ (Just ((s, _), _), Just ((e, _), _)) -> (s, e)+ _ -> assert `failure` caves+ assert (minD <= maxD && fromEnum minD >= 1 `blame` caves) skip+ let gen :: R.StdGen -> LevelId -> (R.StdGen, (LevelId, Level))+ gen g ldepth =+ let (g1, g2) = R.split g+ findG = findGenerator cops caves ldepth minD maxD+ res = St.evalState findG g1+ in (g2, (ldepth, res))+ con :: R.StdGen -> (FreshDungeon, R.StdGen)+ con g = let (gd, levels) = mapAccumL gen g [minD..maxD]+ freshDungeon = EM.fromList levels+ freshDepth = if minD == maxD then 10 else fromEnum maxD+ in (FreshDungeon{..}, gd)+ St.state con
+ Game/LambdaHack/Server/DungeonGen/AreaRnd.hs view
@@ -0,0 +1,150 @@+-- | Operations on the 'Area' type that involve random numbers.+module Game.LambdaHack.Server.DungeonGen.AreaRnd+ ( -- * Picking points inside areas+ xyInArea, mkRoom, mkVoidRoom+ -- * Choosing connections+ , connectGrid, randomConnection+ -- * Plotting corridors+ , Corridor, connectPlaces+ ) where++import qualified Data.EnumSet as ES++import Game.LambdaHack.Common.Area+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Utils.Assert++-- Picking random points inside areas++-- | Pick a random point within an area.+xyInArea :: Area -> Rnd PointXY+xyInArea (x0, y0, x1, y1) = do+ rx <- randomR (x0, x1)+ ry <- randomR (y0, y1)+ return $ PointXY (rx, ry)++-- | Create a random room according to given parameters.+mkRoom :: (X, Y) -- ^ minimum size+ -> Area -- ^ the containing area, not the room itself+ -> Rnd Area+mkRoom (xm, ym) (x0, y0, x1, y1) = do+ let area0 = (x0, y0, x1 - xm + 1, y1 - ym + 1)+ assert (validArea area0 `blame` area0) $ do+ PointXY (rx0, ry0) <- xyInArea area0+ let area1 = (rx0 + xm - 1, ry0 + ym - 1, x1, y1)+ assert (validArea area1 `blame` area1) $ do+ PointXY (rx1, ry1) <- xyInArea area1+ return (rx0, ry0, rx1, ry1)++-- | Create a void room, i.e., a single point area.+mkVoidRoom :: Area -- ^ the area in which to pick the point+ -> Rnd Area+mkVoidRoom area = assert (validArea area `blame` area) $ do+ PointXY (ry, rx) <- xyInArea area+ return (ry, rx, ry, rx)++-- 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 (nx, ny) = do+ let unconnected = ES.fromList [ PointXY (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) ]+ connectGrid' (nx, ny) unconnected candidates []++connectGrid' :: (X, Y) -> ES.EnumSet PointXY -> ES.EnumSet PointXY+ -> [(PointXY, PointXY)]+ -> Rnd [(PointXY, PointXY)]+connectGrid' (nx, ny) unconnected candidates acc+ | ES.null candidates = return $ map sortPointXY acc+ | otherwise = do+ c <- oneOf (ES.toList candidates)+ -- potential new candidates:+ let ns = ES.fromList $ vicinityCardinalXY (0, 0, nx-1, ny-1) c+ nu = ES.delete c unconnected -- new unconnected+ -- (new candidates, potential connections):+ (nc, ds) = ES.partition (`ES.member` nu) ns+ new <- if ES.null ds+ then return id+ else do+ d <- oneOf (ES.toList ds)+ return ((c, d) :)+ connectGrid' (nx, ny) nu+ (ES.delete c (candidates `ES.union` nc)) (new acc)++-- | Pick a single random connection between adjacent areas within a grid.+randomConnection :: (X, Y) -> Rnd (PointXY, PointXY)+randomConnection (nx, ny) =+ assert (nx > 1 && ny > 0 || nx > 0 && ny > 1 `blame` (nx, ny)) $ do+ rb <- oneOf [False, True]+ if rb || ny <= 1+ then do+ rx <- randomR (0, nx-2)+ ry <- randomR (0, ny-1)+ return (PointXY (rx, ry), PointXY (rx+1, ry))+ else do+ rx <- randomR (0, nx-1)+ ry <- randomR (0, ny-2)+ return (PointXY (rx, ry), PointXY (rx, ry+1))++-- Plotting individual corridors between two areas++-- | The choice of horizontal and vertical orientation.+data HV = Horiz | Vert++-- | The coordinates of consecutive fields of a corridor.+type Corridor = [PointXY]++-- | 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+ -> 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)]++-- TODO: assert that sx1 <= tx0, etc.+-- | Try to connect two places with a corridor.+-- Choose entrances at least 4 or 3 tiles distant from the edges, if the place+-- is big enough. Note that with @pfence == FNone@, the area considered+-- is the interior of the place, without the outermost tiles.+connectPlaces :: Area -> Area -> Rnd Corridor+connectPlaces sa@(_, _, sx1, sy1) ta@(tx0, ty0, _, _) = do+ let trim (x0, y0, x1, y1) =+ let trim4 (v0, v1) | v1 - v0 < 6 = (v0, v1)+ | v1 - v0 < 8 = (v0 + 3, v1 - 3)+ | otherwise = (v0 + 4, v1 - 4)+ (nx0, nx1) = trim4 (x0, x1)+ (ny0, ny1) = trim4 (y0, y1)+ in (nx0, ny0, nx1, ny1)+ PointXY (sx, sy) <- xyInArea $ trim sa+ PointXY (tx, ty) <- xyInArea $ trim ta+ let xarea = (sx1+2, min sy ty, tx0-2, max sy ty)+ yarea = (sx, sy1+2, tx, ty0-2)+ xyarea = (sx1+2, sy1+2, tx0-2, ty0-2)+ (hv, area) <- if validArea xyarea+ then fmap (\ hv -> (hv, xyarea)) (oneOf [Horiz, Vert])+ else if validArea xarea+ then return (Horiz, xarea)+ else return (Vert, normalizeArea yarea) -- vertical bias+ let (p0, p1) = case hv of+ Horiz -> (PointXY (if trivialArea sa then sx else sx1 + 1, sy),+ PointXY (if trivialArea ta then tx else tx0 - 1, ty))+ Vert -> (PointXY (sx, if trivialArea sa then sy else sy1 + 1),+ PointXY (tx, if trivialArea ta then ty else ty0 - 1))+ -- 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.+ mkCorridor hv p0 p1 area
+ Game/LambdaHack/Server/DungeonGen/Cave.hs view
@@ -0,0 +1,150 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Generation of caves (not yet inhabited dungeon levels) from cave kinds.+module Game.LambdaHack.Server.DungeonGen.Cave+ ( TileMapXY, ItemFloorXY, Cave(..), buildCave+ ) where++import Control.Monad+import qualified Data.EnumMap.Strict as EM+import qualified Data.List as L++import Game.LambdaHack.Common.Area+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.Misc+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Content.CaveKind+import Game.LambdaHack.Content.TileKind+import Game.LambdaHack.Server.DungeonGen.AreaRnd+import Game.LambdaHack.Server.DungeonGen.Place hiding (TileMapXY)+import qualified Game.LambdaHack.Server.DungeonGen.Place as Place++-- | 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)++-- | 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+ , dplaces :: [Place] -- ^ places generated in the cave+ }+ deriving Show++{-+Rogue cave is generated by an algorithm inspired by the original Rogue,+as follows:++ * The available area is divided into a grid, e.g, 3 by 3,+ where each of the 9 grid cells has approximately the same size.++ * In each of the 9 grid cells one room is placed at a random position+ and with a random size, but larger than The minimum size,+ e.g, 2 by 2 floor tiles.++ * Rooms that are on horizontally or vertically adjacent grid cells+ may be connected by a corridor. Corridors consist of 3 segments of straight+ lines (either "horizontal, vertical, horizontal" or "vertical, horizontal,+ vertical"). They end in openings in the walls of the room they connect.+ It is possible that one or two of the 3 segments have length 0, such that+ the resulting corridor is L-shaped or even a single straight line.++ * Corridors are generated randomly in such a way that at least every room+ on the grid is connected, and a few more might be. It is not sufficient+ to always connect all adjacent rooms.+-}+-- TODO: fix identifier naming and split, after the code grows some more+-- | Cave generation by an algorithm inspired by the original Rogue,+buildCave :: Kind.COps -- ^ content definitions+ -> Int -- ^ depth of the level to generate+ -> 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 }+ , cocave=Kind.Ops{okind} }+ ln depth ci = do+ let kc@CaveKind{..} = okind ci+ lgrid@(gx, gy) <- rollDiceXY cgrid+ lminplace <- rollDiceXY cminPlaceSize+ let gs = grid lgrid (0, 0, cxsize - 1, cysize - 1)+ mandatory1 <- replicateM (cnonVoidMin `div` 2) $+ xyInArea (0, 0, gx `div` 3, gy - 1)+ mandatory2 <- replicateM (cnonVoidMin `divUp` 2) $+ xyInArea (gx - 1 - (gx `div` 3), 0, gx - 1, gy - 1)+ places0 <- mapM (\ (i, r) -> do+ rv <- chance cvoidChance+ r' <- if rv && i `notElem` (mandatory1 ++ mandatory2)+ then mkVoidRoom r+ else mkRoom lminplace r+ return (i, r')) gs+ connects <- connectGrid lgrid+ addedConnects <-+ if gx * gy > 1+ then let caux = round $ cauxConnects * fromIntegral (gx * gy)+ in replicateM caux (randomConnection lgrid)+ else return []+ let allConnects = L.union connects addedConnects -- no duplicates+ places = EM.fromList places0+ cs <- mapM (\ (p0, p1) -> do+ let r0 = places EM.! p0+ r1 = places EM.! p1+ connectPlaces r0 r1) allConnects+ let hardRockId = ouniqGroup "hard rock"+ fenceBounds = (1, 1, cxsize - 2, cysize - 2)+ fence = buildFence hardRockId fenceBounds+ pickedCorTile <- opick ccorridorTile (const True)+ let addPl (m, pls) (_, (x0, _, x1, _)) | x0 == x1 = return (m, pls)+ addPl (m, pls) (_, r) = do+ (tmap, place) <- buildPlace cops kc pickedCorTile ln depth r+ return (EM.union tmap m, place : pls)+ (lplaces, dplaces) <- foldM addPl (fence, []) places0+ let lcorridors = EM.unions (L.map (digCorridors pickedCorTile) cs)+ let lm = EM.unionWith (mergeCorridor cotile) lcorridors lplaces+ -- Convert wall openings into doors, possibly.+ dmap <-+ let f l (p, t) =+ if not $ Tile.hasFeature cotile F.Suspect t+ then return l -- 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+ else do+ doorClosedId <- Tile.changeTo cotile t -- hack 47+ doorOpenId <- Tile.changeTo cotile doorClosedId+ ro <- chance copenChance+ if not ro+ then return $ EM.insert p doorClosedId l+ else return $ EM.insert p doorOpenId l+ in foldM f lm (EM.assocs lm)+ let cave = Cave+ { dkind = ci+ , ditem = EM.empty+ , dmap+ , dplaces+ }+ return cave++digCorridors :: Kind.Id TileKind -> Corridor -> TileMapXY+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)+digCorridors _ _ = EM.empty++mergeCorridor :: Kind.Ops TileKind -> Kind.Id TileKind -> Kind.Id TileKind+ -> Kind.Id TileKind+mergeCorridor cotile _ = Tile.hiddenAs cotile
+ Game/LambdaHack/Server/DungeonGen/Place.hs view
@@ -0,0 +1,183 @@+{-# LANGUAGE RankNTypes #-}+-- | Generation of places from place kinds.+module Game.LambdaHack.Server.DungeonGen.Place+ ( TileMapXY, Place(..), placeValid, buildFence, buildPlace+ ) where++import Data.Binary+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import qualified Data.List as L+import Data.Text (Text)+import qualified Data.Text as T++import Game.LambdaHack.Common.Area+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.PointXY+import Game.LambdaHack.Common.Random+import Game.LambdaHack.Content.CaveKind+import Game.LambdaHack.Content.PlaceKind+import Game.LambdaHack.Content.TileKind+import Game.LambdaHack.Utils.Assert++-- TODO: use more, rewrite as needed, document each field.+-- | The parameters of a place. Most are immutable and set+-- at the time when a place is generated.+data Place = Place+ { qkind :: !(Kind.Id PlaceKind)+ , qarea :: !Area+ , qseen :: !Bool+ , qlegend :: !Text+ , qsolidFence :: !(Kind.Id TileKind)+ , qhollowFence :: !(Kind.Id TileKind)+ }+ deriving Show++instance Binary Place where+ put Place{..} = do+ put qkind+ put qarea+ put qseen+ put qlegend+ put qsolidFence+ put qhollowFence+ get = do+ qkind <- get+ qarea <- get+ qseen <- get+ qlegend <- get+ qsolidFence <- get+ qhollowFence <- get+ return Place{..}++-- | 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)++-- | For @CAlternate@ tiling, require the place be comprised+-- of an even number of whole corners, with exactly one square+-- overlap between consecutive coners and no trimming.+-- For other tiling methods, check that the area is large enough for tiling+-- the corner twice in each direction, with a possible one row/column overlap.+placeValid :: Area -- ^ the area to fill+ -> PlaceKind -- ^ the place kind to construct+ -> Bool+placeValid r PlaceKind{..} =+ let (x0, y0, x1, y1) = expandFence pfence r+ dx = x1 - x0 + 1+ dy = y1 - y0 + 1+ dxcorner = case ptopLeft of [] -> 0 ; l : _ -> T.length l+ dycorner = L.length ptopLeft+ wholeOverlapped d dcorner = d > 1 && dcorner > 1 &&+ (d - 1) `mod` (2 * (dcorner - 1)) == 0+ in case pcover of+ CAlternate -> wholeOverlapped dx dxcorner &&+ wholeOverlapped dy dycorner+ _ -> dx >= 2 * dxcorner - 1 &&+ dy >= 2 * dycorner - 1++-- | Modify available room area according to fence type.+expandFence :: Fence -> Area -> Area+expandFence fence r = case fence of+ FWall -> r+ FFloor -> expand r (-1)+ FNone -> expand r 1++-- | Given a few parameters, roll and construct a 'Place' datastructure+-- and fill a cave section acccording to it.+buildPlace :: Kind.COps -- ^ the game content+ -> CaveKind -- ^ current cave kind+ -> Kind.Id TileKind -- ^ fence tile, if fence hollow+ -> Int -- ^ current level depth+ -> Int -- ^ maximum depth+ -> Area -- ^ interior area of the place+ -> Rnd (TileMapXY, Place)+buildPlace Kind.COps{ cotile=cotile@Kind.Ops{opick=opick}+ , coplace=Kind.Ops{okind=pokind, opick=popick} }+ CaveKind{..} qhollowFence ln depth r+ = assert (not (trivialArea r) `blame` r) $ do+ qsolidFence <- opick cfillerTile (const True)+ dark <- chanceDeep ln depth cdarkChance+ qkind <- popick (T.pack "rogue") (placeValid r)+ let kr = pokind qkind+ qlegend = if dark then cdarkLegendTile else clitLegendTile+ qseen = False+ qarea = expandFence (pfence kr) r+ place = assert (validArea qarea `blame` qarea) Place {..}+ legend <- olegend cotile qlegend+ let xlegend = EM.insert 'X' qhollowFence legend+ return (digPlace place kr xlegend, place)++-- | Roll a legend of a place plan: a map from plan symbols to tile kinds.+olegend :: Kind.Ops TileKind -> Text+ -> Rnd (EM.EnumMap Char (Kind.Id TileKind))+olegend Kind.Ops{ofoldrWithKey, opick} group =+ let getSymbols _ tk acc =+ maybe acc (const $ ES.insert (tsymbol tk) acc)+ (L.lookup group $ tfreq tk)+ symbols = ofoldrWithKey getSymbols ES.empty+ getLegend s acc = do+ m <- acc+ tk <- opick group $ (== s) . tsymbol+ return $ EM.insert s tk m+ legend = ES.fold 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 fenceId (x0, y0, x1, y1) =+ EM.fromList $ [ (PointXY (x, y), fenceId)+ | x <- [x0-1, x1+1], y <- [y0..y1] ] +++ [ (PointXY (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++-- 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+tilePlace (x0, y0, x1, y1) pl@PlaceKind{..} =+ let dx = x1 - x0 + 1+ dy = y1 - y0 + 1+ fromX (x, y) = L.map PointXY $ L.zip [x..] (repeat y)+ fillInterior :: (forall a. Int -> [a] -> [a]) -> [(PointXY, Char)]+ fillInterior f =+ let tileInterior (y, row) = L.zip (fromX (x0, y)) $ f dx row+ reflected = L.zip [y0..] $ f dy $ map T.unpack ptopLeft+ in L.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+ interior = case pcover of+ CAlternate ->+ let tile :: Int -> [a] -> [a]+ tile _ [] = assert `failure` pl+ tile d pat =+ L.take d (L.cycle $ L.init pat ++ L.init (L.reverse pat))+ in fillInterior tile+ CStretch ->+ let stretch :: Int -> [a] -> [a]+ stretch _ [] = assert `failure` pl+ stretch d pat = tileReflect d (pat ++ L.repeat (L.last pat))+ in fillInterior stretch+ CReflect ->+ let reflect :: Int -> [a] -> [a]+ reflect d pat = tileReflect d (L.cycle pat)+ in fillInterior reflect+ in EM.fromList interior
+ Game/LambdaHack/Server/EffectSem.hs view
@@ -0,0 +1,462 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Effect semantics.+-- TODO: document+module Game.LambdaHack.Server.EffectSem+ ( -- + Semantics of effects+ itemEffect, effectSem+ -- * Assorted operations+ , createItems, addHero, spawnMonsters, electLeader, deduceKilled+ ) where++import Control.Monad+import qualified Data.Char as Char+import qualified Data.EnumMap.Strict as EM+import qualified Data.HashMap.Strict as HM+import Data.Key (mapWithKeyM_)+import Data.List+import Data.Maybe+import Data.Ratio ((%))+import Data.Text (Text)+import qualified NLP.Miniutter.English as MU++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import qualified Game.LambdaHack.Common.Color as Color+import qualified Game.LambdaHack.Common.Effect as Effect+import Game.LambdaHack.Common.Faction+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.Random+import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.FactionKind+import Game.LambdaHack.Server.Action+import Game.LambdaHack.Server.Config+import Game.LambdaHack.Server.State+import Game.LambdaHack.Utils.Assert+import Game.LambdaHack.Utils.Frequency++-- + Semantics of effects++-- TODO: when h2h items have ItemId, replace Item with ItemId+-- | The source actor affects the target actor, with a given item.+-- If the event is seen, the item may get identified. This function+-- is mutually recursive with @effect@ and so it's a part of @Effect@+-- semantics.+itemEffect :: (MonadAtomic m, MonadServer m)+ => 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+ when b $ maybe skip atomic miid++-- | The source actor affects the target actor, with a given effect and power.+-- Both actors are on the current level and can be the same actor.+-- The boolean result indicates if the effect was spectacular enough+-- for the actors to identify it (and the item that caused it, if any).+effectSem :: (MonadAtomic m, MonadServer m)+ => Effect.Effect Int -> ActorId -> ActorId+ -> m Bool+effectSem effect source target = case effect of+ Effect.NoEffect -> effectNoEffect+ Effect.Heal p -> effectHeal p target+ Effect.Hurt nDm p -> effectWound nDm p source target+ Effect.Mindprobe _ -> effectMindprobe target+ Effect.Dominate | source /= target -> effectDominate source target+ Effect.Dominate -> effectSem (Effect.Mindprobe undefined) source target+ Effect.CallFriend p -> effectCallFriend p source target+ Effect.Summon p -> effectSummon p target+ Effect.CreateItem p -> effectCreateItem p target+ Effect.ApplyPerfume -> effectApplyPerfume source target+ Effect.Regeneration p -> effectSem (Effect.Heal p) source target+ Effect.Searching p -> effectSearching p source+ Effect.Ascend p -> effectAscend p target+ Effect.Descend p -> effectDescend p target+ Effect.Escape -> effectEscape target++-- + Individual semantic functions for effects++-- ** NoEffect++effectNoEffect :: Monad m => m Bool+effectNoEffect = return False++-- ** Heal++effectHeal :: MonadAtomic m+ => Int -> ActorId -> m Bool+effectHeal power target = do+ Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ tm <- getsState $ getActorBody target+ let bhpMax = maxDice (ahp $ okind $ bkind tm)+ if power > 0 && bhp tm >= bhpMax+ then do+ execSfxAtomic $ EffectD target Effect.NoEffect+ return False+ else do+ let deltaHP = min power (bhpMax - bhp tm)+ execCmdAtomic $ HealActorA target deltaHP+ execSfxAtomic $ EffectD target $ Effect.Heal deltaHP+ return True++-- ** Wound++effectWound :: (MonadAtomic m, MonadServer m)+ => RollDice -> Int -> ActorId -> ActorId+ -> m Bool+effectWound nDm power source target = do+ n <- rndToAction $ rollDice nDm+ let deltaHP = - (n + power)+ if deltaHP >= 0+ then return False+ else do+ -- Damage the target.+ execCmdAtomic $ HealActorA target deltaHP+ execSfxAtomic $ EffectD target $+ if source == target+ then Effect.Heal deltaHP+ else Effect.Hurt nDm deltaHP{-hack-}+ return True++-- ** Mindprobe++effectMindprobe :: MonadAtomic m+ => ActorId -> m Bool+effectMindprobe target = do+ tb <- getsState (getActorBody target)+ let lid = blid tb+ fact <- getsState $ (EM.! bfid tb) . sfactionD+ lb <- getsState $ actorNotProjList (isAtWar fact) lid+ let nEnemy = length lb+ if nEnemy == 0 then do+ execSfxAtomic $ EffectD target Effect.NoEffect+ return False+ else do+ execSfxAtomic $ EffectD target $ Effect.Mindprobe nEnemy+ return True++-- ** Dominate++effectDominate :: (MonadAtomic m, MonadServer m)+ => ActorId -> ActorId -> m Bool+effectDominate source target = do+ Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ sb <- getsState (getActorBody source)+ tb <- getsState (getActorBody target)+ if bfid tb == bfid sb then do+ execSfxAtomic $ EffectD target Effect.NoEffect+ return False+ else do+ -- TODO: Perhaps insert a turn of delay here to allow countermeasures.+ electLeader (bfid tb) (blid tb) target+ ais <- getsState $ getActorItem target+ execCmdAtomic $ LoseActorA target tb ais+ let bNew = tb {bfid = bfid sb}+ execCmdAtomic $ CreateActorA target bNew ais+ leaderOld <- getsState $ gleader . (EM.! bfid sb) . sfactionD+ -- Halve the speed as a side-effect of domination.+ let speed = fromMaybe (aspeed $ okind $ bkind bNew) $ bspeed bNew+ delta = speedScale (1%2) speed+ 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 ()+electLeader fid lid aidDead = do+ mleader <- getsState $ gleader . (EM.! fid) . sfactionD+ when (isNothing mleader || mleader == Just aidDead) $ do+ actorD <- getsState sactorD+ let ours (_, b) = bfid b == fid && not (bproj b)+ party = filter ours $ EM.assocs actorD+ onLevel <- getsState $ actorNotProjAssocs (== fid) lid+ let mleaderNew = listToMaybe $ filter (/= aidDead)+ $ map fst $ onLevel ++ party+ execCmdAtomic $ LeadFactionA fid mleader mleaderNew++deduceKilled :: (MonadAtomic m, MonadServer m) => Actor -> m ()+deduceKilled body = do+ let fid = bfid body+ spawn <- getsState $ isSpawnFaction fid+ summon <- getsState $ isSummonFaction fid+ Config{configFirstDeathEnds} <- getsServer sconfig+ mleader <- getsState $ gleader . (EM.! fid) . sfactionD+ when (not spawn && not summon+ && (isNothing mleader || configFirstDeathEnds)) $+ deduceQuits body $ Status Killed (fromEnum $ blid body) ""++-- ** SummonFriend++effectCallFriend :: (MonadAtomic m, MonadServer m)+ => Int -> ActorId -> ActorId+ -> m Bool+effectCallFriend power source target = do+ Kind.COps{cotile} <- getsState scops+ sm <- getsState (getActorBody source)+ tm <- getsState (getActorBody target)+ ps <- getsState $ nearbyFreePoints cotile (const True) (bpos tm) (blid tm)+ summonFriends (bfid sm) (take power ps) (blid tm)+ return True++summonFriends :: (MonadAtomic m, MonadServer m)+ => FactionId -> [Point] -> LevelId+ -> m ()+summonFriends bfid ps lid = do+ Kind.COps{ coactor=coactor@Kind.Ops{opick}+ , cofact=Kind.Ops{okind} } <- getsState scops+ time <- getsState $ getLocalTime lid+ factionD <- getsState sfactionD+ let fact = okind $ gkind $ factionD EM.! bfid+ forM_ ps $ \ p -> do+ mk <- rndToAction $ opick (fname fact) (const True)+ if mk == heroKindId coactor+ then addHero bfid p lid [] Nothing time+ else addMonster mk bfid p lid time+ -- No leader election needed, bebause an alive actor of the same faction+ -- causes the effect, so there is already a leader.++addActor :: (MonadAtomic m, MonadServer m)+ => Kind.Id ActorKind -> FactionId -> Point -> LevelId -> Int+ -> Maybe Char -> Maybe Text -> Maybe Color.Color -> Time+ -> m ActorId+addActor mk bfid pos lid hp bsymbol bname bcolor time = do+ let m = actorTemplate mk bsymbol bname bcolor Nothing hp Nothing pos lid time+ bfid False+ acounter <- getsServer sacounter+ modifyServer $ \ser -> ser {sacounter = succ acounter}+ execCmdAtomic $ CreateActorA acounter m []+ 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+ Kind.COps{coactor=coactor@Kind.Ops{okind}} <- getsState scops+ Faction{gcolor, gconfig} <- getsState $ (EM.! bfid) . sfactionD+ let kId = heroKindId coactor+ hp <- rndToAction $ rollDice $ ahp $ okind kId+ mhs <- mapM (\n -> getsState $ \s -> tryFindHeroK s bfid n) [0..9]+ let freeHeroK = elemIndex Nothing mhs+ n = fromMaybe (fromMaybe 100 freeHeroK) mNumber+ symbol = if n < 1 || n > 9 then '@' else Char.intToDigit n+ name | gcolor == Color.BrWhite =+ fromMaybe ("Hero" <+> showT n) $ lookup n configHeroNames+ | otherwise = gconfig <+> "Hero" <+> showT n+ startHP = hp - (hp `div` 5) * min 3 n+ addActor+ kId bfid ppos lid startHP (Just symbol) (Just name) (Just gcolor) time++-- ** SpawnMonster++effectSummon :: (MonadAtomic m, MonadServer m)+ => Int -> ActorId -> m Bool+effectSummon power target = do+ Kind.COps{cotile} <- getsState scops+ tm <- getsState (getActorBody target)+ ps <- getsState $ nearbyFreePoints cotile (const True) (bpos tm) (blid tm)+ time <- getsState $ getLocalTime (blid tm)+ spawnMonsters (take power ps) (blid tm) (const True) time "summon"+ 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.+spawnMonsters :: (MonadAtomic m, MonadServer m)+ => [Point] -> LevelId -> ((FactionId, Faction) -> Bool)+ -> Time -> Text+ -> m ()+spawnMonsters ps lid filt time freqChoice = assert (not $ null ps) $ do+ Kind.COps{ coactor=Kind.Ops{opick}+ , cofact=Kind.Ops{okind} } <- getsState scops+ factionD <- getsState sfactionD+ -- TODO: rewrite with opick?+ let f (fid, fact) = let kind = okind (gkind fact)+ g n = (n, (kind, fid))+ in fmap g $ lookup freqChoice $ ffreq kind+ case mapMaybe f $ filter filt $ EM.assocs factionD of+ [] -> return () -- no faction spawns+ spawnList -> do+ let freq = toFreq "spawnMonsters" spawnList+ (spawnKind, bfid) <- rndToAction $ frequency freq+ laid <- forM ps $ \ p -> do+ mk <- rndToAction $ opick (fname spawnKind) (const True)+ addMonster mk bfid p lid time+ mleader <- getsState $ gleader . (EM.! bfid) . sfactionD+ when (isNothing mleader) $+ execCmdAtomic $ LeadFactionA bfid Nothing (Just $ head laid)++-- | Create a new monster on the level, at a given position+-- and with a given actor kind and HP.+addMonster :: (MonadAtomic m, MonadServer m)+ => Kind.Id ActorKind -> FactionId -> Point -> LevelId -> Time+ -> m ActorId+addMonster mk bfid ppos lid time = do+ Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ hp <- rndToAction $ rollDice $ ahp $ okind mk+ addActor mk bfid ppos lid hp Nothing Nothing Nothing time++-- ** CreateItem++effectCreateItem :: (MonadAtomic m, MonadServer m)+ => Int -> ActorId -> m Bool+effectCreateItem power target = do+ tm <- getsState $ getActorBody target+ void $ createItems power (bpos tm) (blid tm)+ return True++createItems :: (MonadAtomic m, MonadServer m)+ => Int -> Point -> LevelId -> m ()+createItems n pos lid = do+ Kind.COps{coitem} <- getsState scops+ flavour <- getsServer sflavour+ discoRev <- getsServer sdiscoRev+ ldepth <- getsLevel lid ldepth+ depth <- getsState sdepth+ replicateM_ n $ do+ (item, k, _) <- rndToAction $ newItem coitem flavour discoRev 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)++-- ** ApplyPerfume++effectApplyPerfume :: MonadAtomic m+ => ActorId -> ActorId -> m Bool+effectApplyPerfume source target =+ if source == target+ then do+ execSfxAtomic $ EffectD target Effect.NoEffect+ return False+ else do+ tm <- getsState $ getActorBody target+ oldSmell <- getsLevel (blid tm) lsmell+ let f p fromSm =+ execCmdAtomic $ AlterSmellA (blid tm) p (Just fromSm) Nothing+ mapWithKeyM_ f oldSmell+ execSfxAtomic $ EffectD target Effect.ApplyPerfume+ return True++-- ** Regeneration++-- ** Searching++effectSearching :: MonadAtomic m => Int -> ActorId -> m Bool+effectSearching power source = do+ execSfxAtomic $ EffectD source $ Effect.Searching power+ return True++-- ** Ascend++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++effLvlGoUp :: MonadAtomic m => ActorId -> Int -> m Bool+effLvlGoUp aid k = do+ Kind.COps{coactor} <- getsState scops+ bOld <- getsState $ getActorBody aid+ let lidOld = blid bOld+ posOld = bpos bOld+ whereto <- getsState $ \s -> whereTo s lidOld k+ case whereto of+ Nothing -> -- We are at the "end" of the dungeon.+ -- TODO: perhaps return Maybe Text explaining why it failed, instead?+ return False+ Just (lidNew, posNew) -> do+ -- The actor is added to the new level, but there can be other actors+ -- at his new position.+ inhabitants <- getsState $ posToActor posNew lidNew+ case inhabitants of+ Nothing -> return ()+ Just aid2 -> do+ -- Start switching places. Move the inhabitant to where the actor is.+ switchLevels aid2 lidOld posOld+ -- Alert about the switch.+ b2 <- getsState $ getActorBody aid2+ let part2 = partActor coactor b2+ verb = "be pushed to another level"+ msg2 = makeSentence [MU.SubjectVerbSg part2 verb]+ execSfxAtomic $ MsgFidD (bfid b2) msg2+ -- There are now two actors at @posOld@.+ switchLevels aid lidNew posNew+ -- The property of at most one actor on a tile is restored.+ void $ getsState $ posToActor posOld lidOld -- assertion is inside+ return True++switchLevels :: MonadAtomic m => ActorId -> LevelId -> Point -> m ()+switchLevels aid lidNew posNew = do+ bOld <- getsState $ getActorBody aid+ ais <- getsState $ getActorItem aid+ let lidOld = blid bOld+ side = bfid bOld+ assert (lidNew /= lidOld `blame` (lidNew, "stairs looped" :: Text)) skip+ -- Sync the actor time with the level time.+ timeOld <- getsState $ getLocalTime lidOld+ timeLastVisited <- getsState $ getLocalTime lidNew+ let delta = timeAdd (btime bOld) (timeNegate timeOld)+ bNew = bOld { blid = lidNew+ , btime = timeAdd timeLastVisited delta+ , bpos = posNew+ , boldpos = posNew} -- new level, new direction+ -- Prevent leader pointing to a non-existing actor.+ mleader <- getsState $ gleader . (EM.! side) . sfactionD+ execCmdAtomic $ LeadFactionA side mleader Nothing+ -- Remove the actor from the old level.+ -- Onlookers see somebody disappear suddenly.+ -- @DestroyActorA@ is too loud, so use @LoseActorA@ instead.+ execCmdAtomic $ LoseActorA aid bOld ais+ -- but this will be fixed just below.+ -- Onlookers see somebody appear suddenly. The actor himself+ -- sees new surroundings and has to reset his perception.+ execCmdAtomic $ CreateActorA aid bNew ais+ -- Inhabitants of the new location not checked, so there may be+ -- two actors at the same position at this point. Beware.+ -- Changing levels is so important, that the leader changes.+ execCmdAtomic $ LeadFactionA side Nothing (Just aid)++-- ** Descend++effectDescend :: MonadAtomic m => Int -> ActorId -> m Bool+effectDescend power target = do+ b <- effLvlGoUp target (-power)+ when b $ execSfxAtomic $ EffectD target $ Effect.Descend power+ return b++-- ** Escape++-- | The faction leaves the dungeon.+effectEscape :: (MonadAtomic m, MonadServer m) => ActorId -> m Bool+effectEscape aid = do+ b <- getsState $ getActorBody aid+ let fid = bfid b+ spawn <- getsState $ isSpawnFaction fid+ summon <- getsState $ isSummonFaction fid+ if spawn || summon then return False+ else do+ deduceQuits b $ Status Escape (fromEnum $ blid b) ""+ return True
+ Game/LambdaHack/Server/Fov.hs view
@@ -0,0 +1,179 @@+-- | Field Of View scanning with a variety of algorithms.+-- See <https://github.com/kosmikus/LambdaHack/wiki/Fov-and-los>+-- for discussion.+module Game.LambdaHack.Server.Fov+ ( dungeonPerception, levelPerception+ , fullscan, FovMode(..)+ ) 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+import Game.LambdaHack.Common.Faction+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.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.TileKind+import Game.LambdaHack.Server.Fov.Common+import qualified Game.LambdaHack.Server.Fov.Digital as Digital+import qualified Game.LambdaHack.Server.Fov.Permissive as Permissive+import qualified Game.LambdaHack.Server.Fov.Shadow as Shadow++newtype PerceptionReachable = PerceptionReachable+ { preachable :: ES.EnumSet Point }+ deriving Show++-- | Calculate perception of the level.+levelPerception :: Kind.COps -> State -> FovMode -> FactionId+ -> LevelId -> Level+ -> 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+ -- 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 {..}++-- | Calculate perception of a faction.+factionPerception :: Kind.COps -> FovMode -> State -> FactionId+ -> FactionPers+factionPerception cops configFov s fid =+ EM.mapWithKey (levelPerception cops s configFov fid) $ 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+ in EM.mapWithKey f $ sfactionD s++-- | A position can be directly lit by an ambient shine or a weak, portable+-- light source, e.g,, carried by a hero. (Only lights of radius 0+-- are considered for now and it's assumed they do not reveal hero's position.+-- TODO: change this to be radius 1 noctovision and introduce stronger+-- light sources that show more but make the hero visible.)+-- A position is visible if it's reachable and either directly lit+-- or adjacent to one that is at once directly lit and reachable.+-- The last condition approximates being+-- on the same side of obstacles as the light source.+-- The approximation is not exact for multiple heroes, but the discrepancy+-- can be attributed to deduction based on combined vague visual hints,+-- e.g., if I don't see the reachable light seen by another hero,+-- 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)++-- | Reachable are all fields on an unblocked path from the hero position.+computeReachable :: Kind.COps -> FovMode -> Level -> Actor+ -> PerceptionReachable+computeReachable Kind.COps{cotile, coactor=Kind.Ops{okind}}+ configFov lvl body =+ let sight = asight $ okind $ bkind body+ fovMode = if sight then configFov else Blind+ ppos = bpos body+ scan = fullscan cotile fovMode ppos lvl+ in PerceptionReachable $ ES.fromList scan++-- | Perform a full scan for a given position. Returns the positions+-- that are currently in the field of view. The Field of View+-- algorithm to use, passed in the second argument, is set in the config file.+-- The actor's own position is considred reachable by him.+fullscan :: Kind.Ops TileKind -- ^ tile content, determines clear tiles+ -> FovMode -- ^ scanning mode+ -> Point -- ^ position of the spectator+ -> Level -- ^ the map that is scanned+ -> [Point]+fullscan cotile fovMode spectatorPos Level{lxsize, ltile} = spectatorPos :+ case fovMode of+ Shadow ->+ L.concatMap (\ tr -> map tr (Shadow.scan (isCl . tr) 1 (0, 1))) tr8+ Permissive ->+ L.concatMap (\ tr -> map tr (Permissive.scan (isCl . tr))) tr4+ Digital r ->+ L.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+ where+ isCl :: Point -> Bool+ isCl = Tile.isClear cotile . (ltile Kind.!)++ trV xy = shift spectatorPos $ toVector lxsize $ VectorXY xy++ -- | 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)+ ]++ -- | 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+ ]++-- 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
@@ -0,0 +1,67 @@+-- | Common definitions for the Field of View algorithms.+-- See <https://github.com/kosmikus/LambdaHack/wiki/Fov-and-los>+-- for some more context and references.+module Game.LambdaHack.Server.Fov.Common+ ( -- * Current scan parameters+ Distance, Progress+ -- * Scanning coordinate system+ , Bump(..)+ -- * Geometry in system @Bump@+ , Line, ConvexHull, Edge, EdgeInterval+ -- * Assorted minor operations+ , maximal, steeper, addHull+ ) where++import qualified Data.List as L++import Game.LambdaHack.Common.PointXY++-- | Distance from the (0, 0) point where FOV originates.+type Distance = Int+-- | Progress along an arc with a constant distance from (0, 0).+type Progress = Int++-- | Rotated and translated coordinates of 2D points, so that the points fit+-- in a single quadrant area (e, g., quadrant I for Permissive FOV, hence both+-- coordinates positive; adjacent diagonal halves of quadrant I and II+-- for Digital FOV, hence y positive).+-- 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)++-- | Straight line between points.+type Line = (Bump, Bump)+-- | Convex hull represented as a list of points.+type ConvexHull = [Bump]+-- | An edge (comprising of a line and a convex hull)+-- of the area to be scanned.+type Edge = (Line, ConvexHull)+-- | The area left to be scanned, delimited by edges.+type EdgeInterval = (Edge, Edge)++-- | 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)++-- | 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)) =+ (yf - y1)*(xf - x2) >= (yf - y2)*(xf - x1)++-- | Extends a convex hull of bumps with a new bump. Nothing needs to be done+-- if the new bump already lies within the hull. The first argument is+-- typically `steeper`, optionally negated, applied to the second argument.+addHull :: (Bump -> Bump -> Bool) -- ^ a comparison function+ -> Bump -- ^ a new bump to consider+ -> ConvexHull -- ^ a convex hull of bumps represented as a list+ -> ConvexHull+addHull gte new = (new :) . go+ where+ go (a:b:cs) | gte a b = go (b:cs)+ go l = l
+ Game/LambdaHack/Server/Fov/Digital.hs view
@@ -0,0 +1,142 @@+-- | DFOV (Digital Field of View) implemented according to specification at <http://roguebasin.roguelikedevelopment.org/index.php?title=Digital_field_of_view_implementation>.+-- This fast version of the algorithm, based on "PFOV", has AFAIK+-- never been described nor implemented before.+module Game.LambdaHack.Server.Fov.Digital+ ( scan, dline, dsteeper, intersect, debugSteeper, debugLine+ ) where++import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Server.Fov.Common+import Game.LambdaHack.Utils.Assert++-- | Calculates the list of tiles, in @Bump@ coordinates, visible from (0, 0),+-- within the given sight range.+scan :: Distance -- ^ visiblity radius+ -> (Bump -> Bool) -- ^ clear tile predicate+ -> [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)]))+ where+ dscan :: Distance -> EdgeInterval -> [Bump]+ dscan d (s0@(sl{-shallow line-}, sBumps0), e@(el{-steep line-}, eBumps)) =+ 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+ -- Corners obstruct view, so the steep line, constructed+ -- from corners, is itself not a part of the view,+ -- 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]]+ 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+ 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+dline p1 p2 =+ assert (uncurry blame $ debugLine (p1, p2)) (p1, p2)++-- | Compare steepness of @(p1, f)@ and @(p2, f)@.+-- Debug: Verify that the results of 2 independent checks are equal.+dsteeper :: Bump -> Bump -> Bump -> Bool+dsteeper f p1 p2 =+ assert (res == debugSteeper f p1 p2) res+ where res = steeper f p1 p2++-- | The X coordinate, represented as a fraction, of the intersection of+-- 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 =+ assert (allB (>= 0) [y, yf])+ ((d - y)*(xf - x) + x*(yf - y), yf - y)+{-+Derivation of the formula:+The intersection point (xt, yt) satisfies the following equalities:+yt = d+(yt - y) (xf - x) = (xt - x) (yf - y)+hence+(yt - y) (xf - x) = (xt - x) (yf - y)+(d - y) (xf - x) = (xt - x) (yf - y)+(d - y) (xf - x) + x (yf - y) = xt (yf - y)+xt = ((d - y) (xf - x) + x (yf - y)) / (yf - y)++General remarks:+A diamond is denoted by its left corner. Hero at (0, 0).+Order of processing in the first quadrant rotated by 45 degrees is+ 45678+ 123+ @+so the first processed diamond is at (-1, 1). The order is similar+as for the restrictive shadow casting algorithm and reversed wrt PFOV.+The line in the curent state of mscan is called the shallow line,+but it's the one that delimits the view from the left, while the steep+line is on the right, opposite to PFOV. We start scanning from the left.++The Point coordinates are cartesian. The Bump coordinates are cartesian,+translated so that the hero is at (0, 0) and rotated so that he always+looks at the first (rotated 45 degrees) quadrant. The (Progress, Distance)+cordinates coincide with the Bump coordinates, unlike in PFOV.+-}++-- | Debug functions for DFOV:++-- | 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)) =+ assert (allB (>= 0) [yf, y1, y2]) $+ let (n1, k1) = intersect (p1, f) 0+ (n2, k2) = intersect (p2, f) 0+ in n1 * k2 >= k1 * n2++-- | Debug: check is a view border line for DFOV is legal.+debugLine :: Line -> (Bool, String)+debugLine line@(B(x1, y1), B(x2, y2))+ | not (allB (>= 0) [y1, y2]) =+ (False, "negative coordinates: " ++ show line)+ | y1 == y2 && x1 == x2 =+ (False, "ill-defined line: " ++ show line)+ | y1 == y2 =+ (False, "horizontal line: " ++ show line)+ | crossL0 =+ (False, "crosses the X axis below 0: " ++ show line)+ | crossG1 =+ (False, "crosses the X axis above 1: " ++ show line)+ | otherwise = (True, "")+ where+ (n, k) = line `intersect` 0+ (q, r) = if k == 0 then (0, 0) else n `divMod` k+ crossL0 = q < 0 -- q truncated toward negative infinity+ crossG1 = q >= 1 && (q > 1 || r /= 0)
+ Game/LambdaHack/Server/Fov/Permissive.hs view
@@ -0,0 +1,152 @@+-- | PFOV (Permissive Field of View) clean-room reimplemented based on the algorithm described in <http://roguebasin.roguelikedevelopment.org/index.php?title=Precise_Permissive_Field_of_View>,+-- though the general structure is more influenced by recursive shadow casting,+-- as implemented in Shadow.hs. In the result, this algorithm is much faster+-- than the original algorithm on dense maps, since it does not scan+-- areas blocked by shadows.+module Game.LambdaHack.Server.Fov.Permissive+ ( scan, dline, dsteeper, intersect, debugSteeper, debugLine+ ) where++import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Server.Fov.Common+import Game.LambdaHack.Utils.Assert++-- TODO: Scanning squares on horizontal lines in octants, not squares+-- on diagonals in quadrants, may be much faster and a bit simpler.+-- Right now we build new view on each end of each visible wall tile+-- and this is necessary only for straight, thin, diagonal walls.++-- | Calculates the list of tiles, in @Bump@ coordinates, visible from (0, 0).+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)]))+ where+ dscan :: Distance -> EdgeInterval -> [Bump]+ dscan d (s0@(sl{-shallow line-}, sBumps0), e@(el{-steep line-}, eBumps)) =+ assert (d >= 0 && pe + 1 >= ps0 && ps0 >= 0+ `blame` (d,s0,e,ps0,pe)) $+ if illegal then [] else inside ++ outside+ where+ (ns, ks) = sl `intersect` d+ (ne, ke) = el `intersect` d+ -- Corners are translucent, so they are invisible, so if intersection+ -- is at a corner, choose pe that creates the smaller view.+ (ps0, pe) = (ns `div` ks, ne `divUp` ke - 1) -- progress interval to check+ -- 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)++ 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++ -- 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+ | 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++ mscan Nothing 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+ -- 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++-- | 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)++-- | Compare steepness of @(p1, f)@ and @(p2, f)@.+-- Debug: Verify that the results of 2 independent checks are equal.+dsteeper :: Bump -> Bump -> Bump -> Bool+dsteeper f p1 p2 =+ assert (res == debugSteeper f p1 p2) res+ where res = steeper f p1 p2++-- | The Y coordinate, represented as a fraction, of the intersection of+-- 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 =+ assert (allB (>= 0) [x, y, xf, yf])+ ((1 + d)*(yf - y) + y*xf - x*yf, (xf - x) + (yf - y))+{-+Derivation of the formula:+The intersection point (xt, yt) satisfies the following equalities:+xt = 1 + d - yt+(yt - y) (xf - x) = (xt - x) (yf - y)+hence+(yt - y) (xf - x) = (xt - x) (yf - y)+yt (xf - x) - y xf = xt (yf - y) - x yf+yt (xf - x) - y xf = (1 + d) (yf - y) - yt (yf - y) - x yf+yt (xf - x) + yt (yf - y) = (1 + d) (yf - y) - x yf + y xf+yt = ((1 + d) (yf - y) + y xf - x yf) / (xf - x + yf - y)++General remarks:+A square is denoted by its bottom-left corner. Hero at (0, 0).+Order of processing in the first quadrant is+9+58+247+@136+so the first processed square is at (0, 1). The order is reversed+wrt the restrictive shadow casting algorithm. The line in the curent state+of mscan is not the steep line, but the shallow line,+and we start scanning from the bottom right.++The Point coordinates are cartesian. The Bump coordinates are cartesian,+translated so that the hero is at (0, 0) and rotated so that he always+looks at the first quadrant. The (Progress, Distance) cordinates+are mangled and not used for geometry.+-}++-- | Debug functions for PFOV:++-- | 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)) =+ assert (allB (>= 0) [xf, yf, x1, y1, x2, y2]) $+ let (n1, k1) = intersect (p1, f) 0+ (n2, k2) = intersect (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))+ | not (allB (>= 0) [x1, y1, x2, y2]) =+ (False, "negative coordinates: " ++ show line)+ | y1 == y2 && x1 == x2 =+ (False, "ill-defined line: " ++ show line)+ | x2 - x1 == - (y2 - y1) =+ (False, "diagonal line: " ++ show line)+ | crossL0 =+ (False, "crosses diagonal below 0: " ++ show line)+ | crossG1 =+ (False, "crosses diagonal above 1: " ++ show line)+ | otherwise = (True, "")+ where+ (n, k) = line `intersect` 0+ (q, r) = if k == 0 then (0, 0) else n `divMod` k+ crossL0 = q < 0 -- q truncated toward negative infinity+ crossG1 = q >= 1 && (q > 1 || r /= 0)
+ Game/LambdaHack/Server/Fov/Shadow.hs view
@@ -0,0 +1,111 @@+-- | A restrictive variant of Recursive Shadow Casting FOV with infinite range.+-- It's not designed for dungeons with diagonal walls and so here+-- they block visibility, though they don't block movement.+-- 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 Data.Ratio++import Game.LambdaHack.Server.Fov.Common+import Game.LambdaHack.Utils.Assert++{-+Field Of View+-------------++The algorithm used is a variant of Shadow Casting. We first compute+fields that are reachable (have unobstructed line of sight) from the hero's+position. Later, in Perception.hs, from this information we compute+the fields that are visible (not hidden in darkness, etc.).++As input to the algorithm, we require information about fields that+block light. As output, we get information on the reachability of all fields.+We assume that the hero is located at position (0, 0)+and we only consider fields (line, row) where line >= 0 and 0 <= row <= line.+This is just about one eighth of the whole hero's surroundings,+but the other parts can be computed in the same fashion by mirroring+or rotating the given algorithm accordingly.++ fov (blocks, maxline) =+ shadow := \empty_set+ reachable (0, 0) := True+ for l \in [ 1 .. maxline ] do+ for r \in [ 0 .. l ] do+ reachable (l, r) := ( \exists a. a \in interval (l, r) \and+ a \not_in shadow)+ if blocks (l, r) then+ shadow := shadow \union interval (l, r)+ end if+ end for+ end for+ return reachable++ interval (l, r) = return [ angle (l + 0.5, r - 0.5),+ angle (l - 0.5, r + 0.5) ]+ angle (l, r) = return atan (r / l)++The algorithm traverses the fields line by line, row by row.+At every moment, we keep in shadow the intervals which are in shadow,+measured by their angle. A square is reachable when any point+in it is not in shadow --- the algorithm is permissive in this respect.+We could also require that a certain fraction of the field is reachable,+or a specific point. Our choice has certain consequences. For instance,+a single blocking field throws a shadow, but the fields immediately behind+the blocking field are still visible.++We can compute the interval of angles corresponding to one square field+by computing the angle of the line passing the upper left corner+and the angle of the line passing the lower right corner.+This is what interval and angle do. If a field is blocking, the interval+for the square is added to the shadow set.+-}++-- | Rotated and translated coordinates of 2D points, so that they fit+-- in the same single octant area.+type SBump = (Progress, Distance)++-- | The area left to be scanned, delimited by fractions of the original arc.+-- Interval @(0, 1)@ means the whole 45 degrees arc of the processed octant+-- is to be scanned.+type Interval = (Rational, Rational)++-- TODO: if ever used, apply static argument transformation to isClear.+-- | Calculates the list of tiles, in @SBump@ coordinates, visible from (0, 0).+scan :: (SBump -> Bool) -- ^ clear tile predicate+ -> Distance -- ^ the current distance from (0, 0)+ -> Interval -- ^ the current interval to scan+ -> [SBump]+scan isClear d (s0, e) =+ let ps = downBias (s0 * fromIntegral d) -- minimal progress to consider+ pe = upBias (e * fromIntegral d) -- maximal progress to consider+ inside = [(p, d) | p <- [ps..pe]]+ outside+ | isClear (ps, d) = mscan (Just s0) ps pe -- start in light+ | otherwise = mscan Nothing ps pe -- start in shadow+ in assert (d >= 0 && e >= 0 && s0 >= 0 && pe >= ps && ps >= 0+ `blame` (d,s0,e,ps,pe)) $+ inside ++ outside+ where+ -- The current state of a scan is kept in @Maybe Rational@.+ -- If it's the @Just@ case, we're in a visible interval. If @Nothing@,+ -- we're in a shadowed interval.+ mscan :: Maybe Rational -> Progress -> Progress -> [SBump]+ mscan (Just s) ps pe+ | s >= e = [] -- empty interval+ | ps > pe = scan isClear (d+1) (s, e) -- reached end, scan next+ | not $ isClear (ps, d) = -- entering shadow+ let ne = (fromIntegral ps - (1%2)) / (fromIntegral d + (1%2))+ in mscan Nothing (ps+1) pe ++ scan isClear (d+1) (s, ne)+ | otherwise = mscan (Just s) (ps+1) pe -- continue in light++ mscan Nothing ps pe+ | ps > pe = [] -- reached end while in shadow+ | isClear (ps, d) = -- moving out of shadow+ let ns = (fromIntegral ps - (1%2)) / (fromIntegral d - (1%2))+ in mscan (Just ns) (ps+1) pe+ | otherwise = mscan Nothing (ps+1) pe -- continue in shadow+++downBias, upBias :: (Integral a, Integral b) => Ratio a -> b+downBias x = round (x - 1 % (denominator x * 3))+upBias x = round (x + 1 % (denominator x * 3))
+ Game/LambdaHack/Server/LoopAction.hs view
@@ -0,0 +1,406 @@+{-# LANGUAGE OverloadedStrings #-}+-- | The main loop of the server, processing human and computer player+-- moves turn by turn.+module Game.LambdaHack.Server.LoopAction (loopSer) where++import Control.Arrow ((&&&))+import Control.Monad+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.List+import Data.Maybe+import qualified Data.Ord as Ord++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 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.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.Time+import Game.LambdaHack.Content.ActorKind+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.Assert++-- | Start a clip (a part of a turn for which one or more frames+-- will be generated). Do whatever has to be done+-- every fixed number of time units, e.g., monster generation.+-- Run the leader and other actors moves. Eventually advance the time+-- and repeat.+loopSer :: (MonadAtomic m, MonadConnServer m)+ => DebugModeSer+ -> (CmdSer -> m Bool)+ -> (FactionId -> ChanFrontend -> ChanServer CmdClientUI -> IO ())+ -> (FactionId -> ChanServer CmdClientAI -> IO ())+ -> Kind.COps+ -> m ()+loopSer sdebugNxt cmdSerSem executorUI executorAI !cops = do+ -- Recover states and launch clients.+ restored <- tryRestore cops+ case restored of+ Nothing -> do -- Starting a new game.+ -- Set up commandline debug mode+ modifyServer $ \ser -> ser {sdebugNxt}+ s <- gameReset cops+ let speedup = speedupCOps (sallClear sdebugNxt)+ execCmdAtomic $ RestartServerA $ updateCOps speedup s+ applyDebug sdebugNxt+ updateConn executorUI executorAI+ initPer+ reinitGame+ -- Save ASAP in case of crashes and disconnects.+ saveBkpAll+ Just (sRaw, ser) -> do -- Running a restored game.+ -- First, set the previous cops, to send consistent info to clients.+ let setPreviousCops = const cops+ execCmdAtomic $ ResumeServerA $ updateCOps setPreviousCops sRaw+ putServer ser {sdebugNxt}+ applyDebug sdebugNxt+ updateConn executorUI executorAI+ initPer+ pers <- getsServer sper+ broadcastCmdAtomic $ \fid -> ResumeA fid (pers EM.! fid)+ -- Second, set the current cops and reinit perception.+ let setCurrentCops = const (speedupCOps (sallClear sdebugNxt) cops)+ -- @sRaw@ is correct here, because none of the above changes State.+ execCmdAtomic $ ResumeServerA $ updateCOps setCurrentCops sRaw+ initPer+ -- Loop, communicating with clients.+ let loop = do+ let factionArena fact = do+ let spawn = isSpawnFact cops fact+ -- TODO; This is a significant advantage of human spawners;+ -- perhaps we could instead auto-switch leaders+ -- to the fist level non-spawner factions act on.+ isHuman = isHumanFact fact+ case gleader fact of+ Just leader | isHuman || not spawn -> do+ b <- getsState $ getActorBody leader+ return $ Just $ blid b+ _ -> return Nothing+ factionD <- getsState sfactionD+ marenas <- mapM factionArena $ EM.elems factionD+ let arenas = ES.toList $ ES.fromList $ catMaybes marenas+ assert (not $ null arenas) skip -- no 2 solo AI spawners scenario+ mapM_ (handleActors cmdSerSem) arenas+ quit <- getsServer squit+ if quit then do+ -- In case of game save+exit or restart, don't age levels (endClip)+ -- since possibly not all actors have moved yet.+ modifyServer $ \ser -> ser {squit = False}+ endOrLoop (updateConn executorUI executorAI) loop+ else do+ endClip arenas+ loop+ loop++saveBkpAll :: (MonadAtomic m, MonadServer m) => m ()+saveBkpAll = do+ execCmdAtomic SaveBkpA+ saveGameBkp++endClip :: (MonadAtomic m, MonadServer m) => [LevelId] -> m ()+endClip arenas = do+ time <- getsState stime+ Config{configSaveBkpClips} <- getsServer sconfig+ let clipN = time `timeFit` timeClip+ cinT = let r = timeTurn `timeFit` timeClip+ in assert (r > 2) r+ bkpFreq = cinT * configSaveBkpClips+ clipMod = clipN `mod` cinT+ bkpSave <- getsServer sbkpSave+ when (bkpSave || clipN `mod` bkpFreq == 0) $ do+ modifyServer $ \ser -> ser {sbkpSave = False}+ execCmdAtomic SaveBkpA+ saveGameBkp+ -- 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.+ when (clipMod == 1) $ do+ arena <- rndToAction $ oneOf arenas+ regenerateLevelHP arena+ generateMonster arena+ -- 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+ -- from regenerateLevelHP, but keep it before AgeGameA.+ -- TODO: this is also needed to keep savefiles small (undo info).+ mapM_ (\lid -> execCmdAtomic $ AgeLevelA lid timeClip) arenas+ execCmdAtomic $ AgeGameA timeClip++-- | Perform moves for individual actors, as long as there are actors+-- with the next move time less than or equal to the current level time.+-- Some very fast actors may move many times a clip and then+-- we introduce subclips and produce many frames per clip to avoid+-- jerky movement. But most often we push exactly one frame or frame delay.+handleActors :: (MonadAtomic m, MonadConnServer m)+ => (CmdSer -> m Bool)+ -> LevelId+ -> m ()+handleActors cmdSerSem lid = do+ Kind.COps{coactor} <- getsState scops+ time <- getsState $ getLocalTime lid -- the end of this clip, inclusive+ prio <- getsLevel lid lprio+ quit <- getsServer squit+ factionD <- getsState sfactionD+ s <- getState+ let -- Actors of the same faction move together.+ -- TODO: insert wrt the order, instead of sorting+ isLeader (aid, b) = Just aid /= gleader (factionD EM.! bfid b)+ order = Ord.comparing $+ ((>= 0) . bhp . snd) &&& bfid . snd &&& isLeader &&& bsymbol . snd+ (atime, as) = EM.findMin prio+ ams = map (\a -> (a, getActorBody a s)) as+ mnext | EM.null prio = Nothing -- no actor alive, wait until it spawns+ | otherwise = if atime > time+ then Nothing -- no actor is ready for another move+ else Just $ minimumBy order ams+ case mnext of+ _ | quit -> return ()+ Nothing -> return ()+ Just (aid, b) | bproj b && bhp b < 0 -> 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+ -- 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@.+ handleActors cmdSerSem lid+ Just (aid, body) -> do+ let side = bfid body+ fact = factionD EM.! side+ mleader = gleader fact+ usesAI = usesAIFact fact+ hasHumanLeader = isNothing $ gAiLeader fact+ queryUI = not usesAI || hasHumanLeader && Just aid == mleader+ -- TODO: check that the command is legal+ cmdS <- if queryUI then+ -- The client always displays a frame in this case.+ sendQueryUI side aid+ else do+ -- Order the UI client (if any) corresponding to the AI client+ -- to display a new frame so that player does not see moves+ -- of all his AI party members cumulated in a single frame,+ -- but one by one.+ execSfxAtomic $ DisplayPushD side+ sendQueryAI side aid+ let leaderNew = aidCmdSer cmdS+ leadAtoms =+ if leaderNew /= aid+ then -- Only leader can change leaders -- TODO: effLvlGoUp changes+ assert (mleader == Just aid)+ [LeadFactionA side mleader (Just leaderNew)]+ else []+ mapM_ execCmdAtomic leadAtoms+ bPre <- getsState $ getActorBody leaderNew+ -- Check if the client cheats, trying to move other faction actors.+ assert (bfid bPre == side `blame` (bPre, side)) skip+ timed <-+ if bhp bPre <= 0 && not (bproj bPre)+ then execFailure side "You strain, fumble and faint from the exertion."+ else cmdSerSem cmdS+ -- AI has to take time, otherwise it'd loop.+ assert (queryUI || timed `blame` (cmdS, timed, bPre)) skip+ -- 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+ -- of domination. We need to generalize by displaying+ -- "(next move in .3s [RET]" when switching leaders.+ -- RET waits .3s and gives back control,+ -- Any other key does the .3s wait and the action from the key+ -- at once.+ when timed $ advanceTime leaderNew+ -- Generate extra frames if the actor has already moved during+ -- this clip, so his multiple moves would be collapsed in one frame.+ -- If the actor changes his speed this very turn, the test can fail,+ -- but it's a minor UI issue, so let it be.+ let previousClipEnd = timeAdd time $ timeNegate timeClip+ lastSingleMove = timeAddFromSpeed coactor bPre previousClipEnd+ when (btime bPre > lastSingleMove) $+ broadcastSfxAtomic DisplayPushD+ 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+ -- 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++-- | 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))+ mapActorItems_ f b++-- | Advance the move time for the given actor.+advanceTime :: MonadAtomic m => ActorId -> m ()+advanceTime aid = do+ Kind.COps{coactor} <- getsState scops+ 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 speed = actorSpeed coactor b+ t = ticksPerMeter speed+ execCmdAtomic $ AgeActorA aid t++-- | Generate a monster, possibly.+generateMonster :: (MonadAtomic m, MonadServer m) => LevelId -> m ()+generateMonster lid = do+ cops <- getsState scops+ pers <- getsServer sper+ lvl@Level{ldepth} <- getsLevel lid id+ s <- getState+ let f fid = isSpawnFaction fid s+ spawns = actorNotProjList f lid s+ depth <- getsState sdepth+ rc <- rndToAction $ monsterGenChance ldepth depth (length spawns)+ when rc $ do+ let allPers = ES.unions $ map (totalVisible . (EM.! lid)) $ EM.elems pers+ pos <- rndToAction $ rollSpawnPos cops allPers lid lvl s+ time <- getsState $ getLocalTime lid+ spawnMonsters [pos] lid (const True) time "spawn"++rollSpawnPos :: Kind.COps -> ES.EnumSet Point -> LevelId -> Level -> State+ -> Rnd Point+rollSpawnPos Kind.COps{cotile} visible lid Level{ltile, lxsize, lysize} s = do+ let factionDist = max lxsize lysize - 5+ inhabitants = actorNotProjList (const True) lid s+ isLit = Tile.isLit cotile+ distantAtLeast d p _ =+ all (\b -> chessDist lxsize (bpos b) p > d) inhabitants+ findPosTry 40 ltile+ [ \ _ t -> not (isLit t) -- no such tiles on some maps+ , distantAtLeast factionDist+ , distantAtLeast $ factionDist `div` 2+ , \ p _ -> not $ p `ES.member` visible+ , distantAtLeast $ factionDist `div` 3+ , \ _ t -> Tile.hasFeature cotile F.CanActor t -- in reachable area+ , distantAtLeast $ factionDist `div` 4+ , distantAtLeast 3 -- otherwise a fast actor can walk and hit in one turn+ , \ p t -> Tile.hasFeature cotile F.Walkable t+ && unoccupied (actorList (const True) lid s) p+ ]++-- TODO: generalize to any list of items (or effects) applied to all actors+-- every turn. Specify the list per level in config.+-- TODO: use itemEffect or at least effectSem to get from Regeneration+-- to HealActorA. Also, Applying an item with Regeneration should do the same+-- 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,+-- 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.+regenerateLevelHP :: MonadAtomic m => LevelId -> m ()+regenerateLevelHP lid = do+ Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ time <- getsState $ getLocalTime lid+ s <- getState+ let approve (a, m) =+ let ak = okind $ bkind m+ itemAssocs = getActorItem a s+ regen = max 1 $+ aregen ak `div`+ case strongestRegen itemAssocs of+ Just (k, _) -> k + 1+ Nothing -> 1+ bhpMax = maxDice (ahp ak)+ deltaHP = min 1 (bhpMax - bhp m)+ in if (time `timeFit` timeTurn) `mod` regen /= 0+ || deltaHP <= 0+ || bhp m <= 0+ then Nothing+ else Just a+ toRegen <-+ getsState $ mapMaybe approve . actorNotProjAssocs (const True) lid+ mapM_ (\aid -> execCmdAtomic $ HealActorA aid 1) toRegen++-- | Continue or exit or restart the game.+endOrLoop :: (MonadAtomic m, MonadConnServer m) => m () -> m () -> m ()+endOrLoop updConn loopServer = do+ cops <- getsState scops+ factionD <- getsState sfactionD+ let inGame fact = case gquit fact of+ Nothing -> True+ Just Status{stOutcome=Camping} -> True+ _ -> False+ gameOver = not $ any inGame $ EM.elems factionD+ let getQuitter fact = case gquit fact of+ Just Status{stOutcome=Restart, stInfo} -> Just stInfo+ _ -> Nothing+ quitters = mapMaybe getQuitter $ EM.elems factionD+ let isCamper fact = case gquit fact of+ Just Status{stOutcome=Camping} -> True+ _ -> False+ campers = filter (isCamper . snd) $ EM.assocs factionD+ case (quitters, campers) of+ (t : _, _) -> do+ modifyServer $ \ser -> ser {scenario = t}+ restartGame updConn loopServer+ _ | gameOver -> restartGame updConn loopServer+ (_, []) -> loopServer -- continue current game+ (_, _ : _) -> do -- save game and exit+ -- Wipe out the quit flag for the savegame files.+ mapM_ (\(fid, fact) ->+ execCmdAtomic+ $ QuitFactionA fid Nothing (gquit fact) Nothing) campers+ -- Save client and server data.+ execCmdAtomic SaveExitA+ saveGameSer+ -- Kill all clients, including those that did not take part+ -- in the current game.+ -- Clients exit not now, but after they print all ending screens.+ killAllClients+ -- Verify that the saved perception is equal to future reconstructed.+ persSaved <- getsServer sper+ configFov <- fovMode+ pers <- getsState $ dungeonPerception cops configFov+ assert (persSaved == pers `blame` (persSaved, pers)) skip+ -- Don't call @loopServer@, that is, quit the game loop.++restartGame :: (MonadAtomic m, MonadConnServer m)+ => m () -> m () -> m ()+restartGame updConn loopServer = do+ cops <- getsState scops+ s <- gameReset cops+ execCmdAtomic $ RestartServerA s+ updConn+ initPer+ reinitGame+ -- Save ASAP in case of crashes and disconnects.+ saveBkpAll+ loopServer
+ Game/LambdaHack/Server/ServerSem.hs view
@@ -0,0 +1,445 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Semantics of 'CmdSer' server commands.+-- A couple of them do not take time, the rest does.+-- Note that since the results are atomic commands, which are executed+-- only later (on the server and some of the clients), all condition+-- are checkd by the semantic functions in the context of the state+-- before the server command. Even if one or more atomic actions+-- are already issued by the point an expression is evaluated, they do not+-- influence the outcome of the evaluation.+-- TODO: document+module Game.LambdaHack.Server.ServerSem where++import Control.Monad+import qualified Data.EnumMap.Strict as EM+import Data.Key (mapWithKeyM_)+import Data.Maybe+import Data.Ratio+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import qualified Game.LambdaHack.Common.Color as Color+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.Msg+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.Common.Vector+import Game.LambdaHack.Content.ActorKind+import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Content.TileKind as TileKind+import Game.LambdaHack.Server.Action hiding (sendQueryAI, sendQueryUI,+ sendUpdateAI, sendUpdateUI)+import Game.LambdaHack.Server.Config+import Game.LambdaHack.Server.EffectSem+import Game.LambdaHack.Server.State+import Game.LambdaHack.Utils.Assert++execFailure :: MonadAtomic m => FactionId -> Msg -> m Bool+execFailure fid msg = do+ execSfxAtomic $ MsgFidD fid msg+ return False++broadcastCmdAtomic :: MonadAtomic m+ => (FactionId -> CmdAtomic) -> m ()+broadcastCmdAtomic fcmd = do+ factionD <- getsState sfactionD+ mapWithKeyM_ (\fid _ -> execCmdAtomic $ fcmd fid) factionD++broadcastSfxAtomic :: MonadAtomic m+ => (FactionId -> SfxAtomic) -> m ()+broadcastSfxAtomic fcmd = do+ factionD <- getsState sfactionD+ mapWithKeyM_ (\fid _ -> execSfxAtomic $ fcmd fid) factionD++-- * MoveSer++-- | Actor moves or attacks or searches or opens doors.+-- Note that client can't determine which of these actions is chosen,+-- because foes can be invisible, doors hidden, clients can move+-- simultaneously during the same turn, etc. Also, only the server+-- is authorized to check if a move is legal and it needs full context+-- for that, e.g., the initial actor position to check if melee attack+-- does not try to reach to a distant tile.+moveSer :: (MonadAtomic m, MonadServer m)+ => ActorId -> Vector -> Bool -> m Bool+moveSer aid dir exploration = do+ cops <- getsState scops+ sm <- getsState $ getActorBody aid+ lvl <- getsLevel (blid sm) id+ let spos = bpos sm -- source position+ tpos = spos `shift` dir -- target position+ -- We start by looking at the target position.+ let lid = blid sm+ tgt <- getsState (posToActor tpos lid)+ case tgt of+ Just target -> do+ -- Attacking does not require full access, adjacency is enough.+ actorAttackActor aid target+ return True+ Nothing+ | accessible cops lvl spos tpos -> do+ execCmdAtomic $ MoveActorA aid spos tpos+ addSmell aid+ return True+ | otherwise -> -- try to open a door or explore a possible door+ actorOpenDoor aid dir exploration++-- 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.+addSmell :: MonadAtomic m => ActorId -> m ()+addSmell aid = do+ Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops+ b <- getsState $ getActorBody aid+ spawn <- getsState $ isSpawnFaction (bfid b)+ let canSmell = asmell $ okind $ bkind b+ unless (bproj b || spawn || canSmell) $ do+ time <- getsState $ getLocalTime $ blid b+ oldS <- getsLevel (blid b) $ EM.lookup (bpos b) . lsmell+ let newTime = timeAdd time smellTimeout+ execCmdAtomic $ AlterSmellA (blid b) (bpos b) oldS (Just newTime)++-- | Resolves the result of an actor moving into another.+-- Actors on blocked positions can be attacked without any restrictions.+-- 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.+actorAttackActor :: (MonadAtomic m, MonadServer m)+ => ActorId -> ActorId -> m ()+actorAttackActor source target = do+ cops@Kind.COps{coitem=Kind.Ops{opick, okind}} <- getsState scops+ sm <- getsState (getActorBody source)+ tm <- getsState (getActorBody target)+ let sfid = bfid sm+ tfid = bfid tm+ time <- getsState $ getLocalTime (blid tm)+ s <- getState+ itemAssocs <- getsState $ getActorItem source+ (miid, item) <-+ if bproj sm+ then case itemAssocs of+ [(iid, item)] -> return (Just iid, item) -- projectile+ _ -> assert `failure` itemAssocs+ else case strongestSword cops itemAssocs of+ Just (_, (iid, w)) -> return (Just iid, w)+ Nothing -> do -- hand to hand combat+ let h2hGroup | isSpawnFaction sfid s = "monstrous"+ | otherwise = "unarmed"+ h2hKind <- rndToAction $ opick h2hGroup (const True)+ flavour <- getsServer sflavour+ discoRev <- getsServer sdiscoRev+ let kind = okind h2hKind+ effect = fmap (maxDice . fst) (ieffect kind)+ return ( Nothing+ , buildItem flavour discoRev h2hKind kind effect )+ let performHit block = do+ let hitA = if block then HitBlockD else HitD+ execSfxAtomic $ StrikeD source target item hitA+ -- Deduct a hitpoint for a pierce of a projectile.+ when (bproj sm) $ execCmdAtomic $ HealActorA source (-1)+ -- Msgs inside itemEffectSem 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 tm time && not (bproj sm) && bhp tm > 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 sm || bproj tm+ fromDipl = EM.findWithDefault Unknown tfid (gdipl sfact)+ unless (friendlyFire || isAtWar sfact tfid || sfid == tfid) $+ execCmdAtomic $ DiplFactionA sfid tfid fromDipl War++-- TODO: bumpTile tpos F.Openable+-- | An actor opens a door.+actorOpenDoor :: (MonadAtomic m, MonadServer m)+ => ActorId -> Vector -> Bool -> m Bool+actorOpenDoor aid dir exploration = do+ Kind.COps{cotile} <- getsState scops+ body <- getsState $ getActorBody aid+ let dpos = shift (bpos body) dir -- the position we act upon+ lid = blid body+ lvl <- getsLevel lid id+ let serverTile = lvl `at` dpos+ freshClientTile = hideTile cotile dpos lvl+ -- TODO: running doesn't open doors if they are hidden,+ -- even if known to the actor. No apparent way to solve that.+ t | exploration = serverTile -- will be found+ | otherwise = freshClientTile -- won't be searched+ -- Try to open the door.+ if Tile.hasFeature cotile F.Openable t+ then triggerSer aid dpos -- searches, too+ else do+ when (exploration && serverTile /= freshClientTile) $+ execCmdAtomic $ SearchTileA aid dpos freshClientTile serverTile+ if Tile.hasFeature cotile F.Closable t+ then execFailure (bfid body) "already open"+ else if exploration && serverTile /= freshClientTile+ then return True -- searching costs+ -- TODO: don't add to history (add a flag to report msgs)+ else execFailure (bfid body) "never mind" -- free bump++-- * RunSer++-- | Actor moves or swaps position with others or opens doors.+runSer :: (MonadAtomic m, MonadServer m) => ActorId -> Vector -> m Bool+runSer aid dir = do+ cops <- getsState scops+ sm <- getsState $ getActorBody aid+ lvl <- getsLevel (blid sm) id+ let spos = bpos sm -- source position+ tpos = spos `shift` dir -- target position+ -- We start by looking at the target position.+ let lid = blid sm+ tgt <- getsState (posToActor tpos lid)+ case tgt of+ Just target+ | accessible cops lvl spos tpos -> do+ -- Switching positions requires full access.+ displaceActor aid target+ return True+ | otherwise ->+ execFailure (bfid sm) "blocked"+ Nothing+ | accessible cops lvl spos tpos -> do+ execCmdAtomic $ MoveActorA aid spos tpos+ addSmell aid+ return True+ | otherwise ->+ actorOpenDoor aid dir False -- no exploration when running++-- | When an actor runs (not walks) into another, they switch positions.+displaceActor :: MonadAtomic m+ => ActorId -> ActorId -> m ()+displaceActor source target = do+ execCmdAtomic $ DisplaceActorA source target+ addSmell source+-- leader <- getsClient getLeader+-- if Just source == leader+-- TODO: The actor will stop running due to the message as soon as running+-- is fixed to check the message before it goes into history.+-- then stopRunning -- do not switch positions repeatedly+-- else void $ focusIfOurs target++-- * 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.+waitSer :: MonadAtomic m => ActorId -> m ()+waitSer aid = do+ Kind.COps{coactor} <- getsState scops+ body <- getsState $ getActorBody aid+ time <- getsState $ getLocalTime $ blid body+ let fromWait = bwait body+ toWait = timeAddFromSpeed coactor body time+ execCmdAtomic $ WaitActorA aid fromWait toWait++-- * PickupSer++pickupSer :: MonadAtomic m+ => ActorId -> ItemId -> Int -> InvChar -> m ()+pickupSer aid iid k l = assert (k > 0 `blame` (aid, iid, k, l)) $ do+ b <- getsState $ getActorBody aid+ execCmdAtomic $ MoveItemA iid k (CFloor (blid b) (bpos b)) (CActor aid l)++-- * DropSer++dropSer :: MonadAtomic m => ActorId -> ItemId -> m ()+dropSer aid iid = do+ b <- getsState $ getActorBody aid+ let k = 1+ execCmdAtomic $ MoveItemA iid k (actorContainer aid (binv b) iid)+ (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+ -> Int -- ^ digital line parameter+ -> ItemId -- ^ the item to be projected+ -> Container -- ^ whether the items comes from floor or inventory+ -> m Bool+projectSer source tpos eps iid container = do+ cops <- getsState scops+ sm <- getsState (getActorBody source)+ Actor{btime} <- getsState $ getActorBody source+ lvl <- getsLevel (blid sm) id+ lxsize <- getsLevel (blid sm) lxsize+ lysize <- getsLevel (blid sm) lysize+ let spos = bpos sm+ lid = blid sm+ -- A bit later than actor time, to prevent a move this turn.+ time = btime `timeAdd` timeEpsilon+ -- TODO: AI should choose the best eps.+ bl = bla lxsize lysize eps spos tpos+ case bl of+ Nothing -> execFailure (bfid sm) "cannot zap oneself"+ Just [] -> assert `failure`+ (spos, tpos, "project from the edge of level" :: Text)+ Just path@(pos:_) -> do+ inhabitants <- getsState (posToActor pos lid)+ if accessible cops lvl spos pos && isNothing inhabitants+ then do+ execSfxAtomic $ ProjectD source iid+ projId <- addProjectile iid pos (blid sm) (bfid sm) path time+ execCmdAtomic+ $ MoveItemA iid 1 container (CActor projId (InvChar 'a'))+ return True+ else+ execFailure (bfid sm) "blocked"++-- | Create a projectile actor containing the given missile.+addProjectile :: (MonadAtomic m, MonadServer m)+ => ItemId -> Point -> LevelId -> FactionId -> [Point] -> Time+ -> m ActorId+addProjectile iid bpos blid bfid path btime = do+ Kind.COps{coactor, coitem=coitem@Kind.Ops{okind}} <- getsState scops+ disco <- getsServer sdisco+ item <- getsState $ getItemBody iid+ let ik = okind (fromJust $ jkind disco item)+ speed = speedFromWeight (iweight ik) (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 path+ m = actorTemplate (projectileKindId coactor) Nothing (Just name) Nothing+ (Just speed) 0 (Just dirPath) bpos blid btime bfid True+ acounter <- getsServer sacounter+ modifyServer $ \ser -> ser {sacounter = succ acounter}+ execCmdAtomic $ CreateActorA acounter m [(iid, item)]+ return acounter++-- * ApplySer++applySer :: (MonadAtomic m, MonadServer m)+ => ActorId -- ^ actor applying the item (is on current level)+ -> ItemId -- ^ the item to be applied+ -> Container -- ^ the location of the item+ -> m ()+applySer actor iid container = do+ item <- getsState $ getItemBody iid+ execSfxAtomic $ ActivateD actor iid+ itemEffect actor actor (Just iid) item+ -- TODO: don't destroy if not really used up; also, don't take time?+ execCmdAtomic $ DestroyItemA iid item 1 container++-- * TriggerSer++-- | Perform the action specified for the tile in case it's triggered.+triggerSer :: (MonadAtomic m, MonadServer m)+ => ActorId -> Point -> m Bool+triggerSer aid dpos = do+ Kind.COps{cotile=cotile@Kind.Ops{okind, opick}} <- getsState scops+ b <- getsState $ getActorBody aid+ let lid = blid b+ lvl <- getsLevel lid id+ let serverTile = lvl `at` dpos+ freshClientTile = hideTile cotile dpos lvl+ when (serverTile /= freshClientTile) $+ -- Search, in case some actors (of other factions?) don't know this tile.+ execCmdAtomic $ SearchTileA aid dpos freshClientTile serverTile+ let f feat =+ case feat of+ F.Cause ef -> do+ -- No block against tile, hence unconditional.+ execSfxAtomic $ TriggerD aid dpos feat {-TODO-}True+ void $ effectSem ef aid aid+ return True+ F.ChangeTo tgroup -> do+ execSfxAtomic $ TriggerD aid dpos feat {-TODO-}True+ as <- getsState $ actorList (const True) lid+ if EM.null $ lvl `atI` dpos+ then if unoccupied as dpos+ then do+ toTile <- rndToAction $ opick tgroup (const True)+ execCmdAtomic $ AlterTileA lid dpos serverTile toTile+ return True+-- TODO: take care of AI using this function (aborts on some of the features, succes on others, etc.)+ else execFailure (bfid b) "blocked" -- by actors+ else execFailure (bfid b) "jammed" -- by items+ _ -> return False+ bs <- mapM f $ TileKind.tfeature $ okind serverTile+ return $! or bs -- TODO: stop after first failure, probably++-- * SetPathSer++setPathSer :: (MonadAtomic m, MonadServer m)+ => ActorId -> [Vector] -> m ()+setPathSer aid path = do+ when (length path <= 2) $ do+ fromColor <- getsState $ bcolor . getActorBody aid+ let toColor = Just 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 False+ execCmdAtomic $ PathActorA aid fromPath (Just lv)++-- * GameRestart++gameRestartSer :: (MonadAtomic m, MonadServer m) => ActorId -> Text -> m ()+gameRestartSer aid stInfo = do+ b <- getsState $ getActorBody aid+ let fid = bfid b+ oldSt <- getsState $ gquit . (EM.! fid) . sfactionD+ modifyServer $ \ser -> ser {squit = True} -- do this at once+ 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+ b <- getsState $ getActorBody aid+ let fid = bfid b+ oldSt <- getsState $ gquit . (EM.! fid) . sfactionD+ modifyServer $ \ser -> ser {squit = True} -- do this at once+ execCmdAtomic $ QuitFactionA fid (Just b) oldSt+ $ Just $ Status Camping (fromEnum $ blid b) ""++-- * GameSaveSer++gameSaveSer :: MonadServer m => m ()+gameSaveSer = do+ modifyServer $ \ser -> ser {sbkpSave = True}+ modifyServer $ \ser -> ser {squit = True} -- do this at once++-- * CfgDumpSer++cfgDumpSer :: (MonadAtomic m, MonadServer m) => ActorId -> m ()+cfgDumpSer aid = do+ b <- getsState $ getActorBody aid+ let fid = bfid b+ Config{configRulesCfgFile} <- getsServer sconfig+ let fn = configRulesCfgFile ++ ".dump"+ msg = "Server dumped current game rules configuration to file"+ <+> T.pack fn <> "."+ dumpCfg fn+ -- Wait with confirmation until saved; tell where the file is.+ execSfxAtomic $ MsgFidD fid msg
+ Game/LambdaHack/Server/StartAction.hs view
@@ -0,0 +1,248 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Operations for starting and restarting the game.+module Game.LambdaHack.Server.StartAction+ ( applyDebug, gameReset, reinitGame, initPer+ ) where++import Control.Monad+import qualified Control.Monad.State as St+import qualified Data.Char as Char+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.Key (mapWithKeyM_)+import qualified Data.Map.Strict as M+import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T+import Data.Tuple (swap)++import Game.LambdaHack.Common.Action+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.AtomicCmd+import qualified Game.LambdaHack.Common.Color as Color+import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Feature as F+import Game.LambdaHack.Common.Flavour+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.Random+import Game.LambdaHack.Common.State+import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.FactionKind+import Game.LambdaHack.Content.ItemKind+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+import Game.LambdaHack.Server.ServerSem+import Game.LambdaHack.Server.State+import Game.LambdaHack.Utils.Assert++-- | Apply debug options that don't need a new game.+applyDebug :: MonadServer m => DebugModeSer -> m ()+applyDebug sdebugNxt =+ modifyServer $ \ser ->+ ser {sdebugSer = (sdebugSer ser) { sniffIn = sniffIn sdebugNxt+ , sniffOut = sniffOut sdebugNxt+ , sallClear = sallClear sdebugNxt+ , stryFov = stryFov sdebugNxt }}++initPer :: MonadServer m => m ()+initPer = do+ cops <- getsState scops+ configFov <- fovMode+ pers <- getsState $ dungeonPerception cops configFov+ modifyServer $ \ser1 -> ser1 {sper = pers}++reinitGame :: (MonadAtomic m, MonadServer m) => m ()+reinitGame = do+ 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.+ -- The biggest part is content, which really needs to be updated+ -- at this point to keep clients in sync with server improvements.+ fromGlobal <- getsState localFromGlobal+ s <- getState+ let defLoc | knowMap = s+ | otherwise = fromGlobal+ discoS <- getsServer sdisco+ let misteriousSymbols = ritemProject $ Kind.stdRuleset corule+ sdisco = let f ik = isymbol (okind ik) `notElem` misteriousSymbols+ in EM.filter f discoS+ sdebugCli <- getsServer $ sdebugCli . sdebugSer+ t <- getsServer scenario+ broadcastCmdAtomic+ $ \fid -> RestartA fid sdisco (pers EM.! fid) defLoc sdebugCli t+ populateDungeon++mapFromInvFuns :: (Bounded a, Enum a, Ord b) => [a -> b] -> M.Map b a+mapFromInvFuns =+ let fromFun f m1 =+ let invAssocs = map (\c -> (f c, c)) [minBound..maxBound]+ m2 = M.fromList invAssocs+ in m2 `M.union` m1+ in foldr fromFun M.empty++lowercase :: Text -> Text+lowercase = T.pack . map Char.toLower . T.unpack++createFactions :: Kind.COps -> Players -> Rnd FactionDict+createFactions Kind.COps{ cofact=Kind.Ops{opick, okind}+ , costrat=Kind.Ops{opick=sopick} } players = do+ let rawCreate isHuman Player{ playerName = gconfig+ , playerKind+ , playerInitial = ginitial+ , playerEntry = gentry } = do+ let cmap = mapFromInvFuns+ [colorToTeamName, colorToPlainName, colorToFancyName]+ nameoc = lowercase gconfig+ prefix | isHuman = "Human"+ | otherwise = "Autonomous"+ (gcolor, gname) = case M.lookup nameoc cmap of+ Nothing -> (Color.BrWhite, prefix <+> gconfig)+ Just c -> (c, prefix <+> gconfig <+> "Team")+ gkind <- opick playerKind (const True)+ let fk = okind gkind+ gdipl = EM.empty -- fixed below+ gquit = Nothing+ gAiLeader <-+ if isHuman+ then return Nothing+ else fmap Just $ sopick (fAiLeader fk) (const True)+ gAiMember <- fmap Just $ sopick (fAiMember fk) (const True)+ let gleader = Nothing+ return Faction{..}+ lHuman <- mapM (rawCreate True) (playersHuman players)+ lComputer <- mapM (rawCreate False) (playersComputer players)+ let lFs = reverse (zip [toEnum (-1), toEnum (-2)..] lComputer) -- sorted+ ++ zip [toEnum 1..] lHuman+ swapIx l =+ let ixs =+ let f (name1, name2) =+ [ (ix1, ix2) | (ix1, fact1) <- lFs, gconfig fact1 == name1+ , (ix2, fact2) <- lFs, gconfig fact2 == name2]+ in concatMap f l+ -- Only symmetry is ensured, everything else is permitted, e.g.,+ -- a faction in alliance with two others that are at war.+ in ixs ++ map swap ixs+ mkDipl diplMode =+ let f (ix1, ix2) =+ let adj fact = fact {gdipl = EM.insert ix2 diplMode (gdipl fact)}+ in EM.adjust adj ix1+ in foldr f+ rawFs = EM.fromDistinctAscList lFs+ -- War overrides alliance, so 'warFs' second.+ allianceFs = mkDipl Alliance rawFs (swapIx (playersAlly players))+ warFs = mkDipl War allianceFs (swapIx (playersEnemy players))+ return warFs++gameReset :: MonadServer m => Kind.COps -> m State+gameReset cops@Kind.COps{coitem, corule} = 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+ t <- getsServer scenario+ scoreTable <- restoreScore sconfig+ let rnd :: Rnd (FactionDict, FlavourMap, Discovery, DiscoRev,+ DungeonGen.FreshDungeon)+ rnd = do+ let scenario = case M.lookup t $ configScenario sconfig of+ Just sc -> sc+ Nothing -> assert `failure` "no scenario configuration:" <+> t+ dng = scenarioDungeon scenario+ caves = case M.lookup dng $ configCaves sconfig of+ Just cv -> cv+ Nothing -> assert `failure` "no caves configuration:" <+> dng+ plr = scenarioPlayers scenario+ players = case M.lookup plr $ configPlayers sconfig of+ Just pl -> pl+ Nothing -> assert `failure` "no players configuration:" <+> plr+ faction <- createFactions cops players+ sflavour <- dungeonFlavourMap coitem+ (sdisco, sdiscoRev) <- serverDiscos coitem+ freshDng <- DungeonGen.dungeonGen cops caves+ return (faction, sflavour, sdisco, sdiscoRev, freshDng)+ let (faction, sflavour, sdisco, sdiscoRev, DungeonGen.FreshDungeon{..}) =+ St.evalState rnd dungeonSeed+ defState = defStateGlobal freshDungeon freshDepth faction cops scoreTable+ defSer = emptyStateServer+ {sdisco, sdiscoRev, sflavour, srandom, scenario = t, sconfig}+ sdebugNxt <- getsServer sdebugNxt+ putServer defSer {sdebugNxt, sdebugSer = sdebugNxt}+ return defState++-- Spawn initial actors. Clients should notice this, to set their leaders.+populateDungeon :: (MonadAtomic m, MonadServer m) => m ()+populateDungeon = do+ cops@Kind.COps{cotile} <- getsState scops+ let initialItems lid (Level{ltile, litemNum}) =+ replicateM litemNum $ do+ pos <- rndToAction+ $ findPos ltile (const (Tile.hasFeature cotile F.CanItem))+ createItems 1 pos lid+ dungeon <- getsState sdungeon+ mapWithKeyM_ initialItems dungeon+ factionD <- getsState sfactionD+ Config{configHeroNames} <- getsServer sconfig+ let (minD, maxD) =+ case (EM.minViewWithKey dungeon, EM.maxViewWithKey dungeon) of+ (Just ((s, _), _), Just ((e, _), _)) -> (s, e)+ _ -> assert `failure` dungeon+ needInitialCrew = EM.assocs factionD+ getEntryLevel (_, fact) = max minD $ min maxD $ gentry fact+ arenas = ES.toList $ ES.fromList $ map getEntryLevel needInitialCrew+ initialActors lid = do+ lvl <- getsLevel lid id+ let arenaFactions = filter ((== lid) . getEntryLevel) needInitialCrew+ entryPoss <- rndToAction+ $ findEntryPoss cops lvl (length arenaFactions)+ mapM_ (arenaActors lid) $ zip arenaFactions entryPoss+ arenaActors _ ((_, Faction{ginitial = 0}), _) = return ()+ arenaActors lid ((side, fact@Faction{ginitial}), ppos) = do+ time <- getsState $ getLocalTime lid+ let ntime = timeAdd time (timeScale timeClip (fromEnum side))+ psFree <-+ getsState $ nearbyFreePoints+ cotile (Tile.hasFeature cotile F.CanActor) ppos lid+ let ps = take ginitial $ zip [0..] psFree+ forM_ ps $ \ (n, p) ->+ if isSpawnFact cops fact+ then spawnMonsters [p] lid ((== side) . fst) ntime "spawn"+ else do+ aid <- addHero side p lid configHeroNames (Just n) ntime+ mleader <- getsState $ gleader . (EM.! side) . sfactionD+ when (isNothing mleader) $+ execCmdAtomic $ LeadFactionA side Nothing (Just aid)+ mapM_ initialActors arenas++-- | Find starting postions for all factions. Try to make them distant+-- from each other. If only one faction, also move it away from any stairs.+findEntryPoss :: Kind.COps -> Level -> Int -> Rnd [Point]+findEntryPoss Kind.COps{cotile} Level{ltile, lxsize, lysize, lstair} k =+ let factionDist = max lxsize lysize - 5+ dist poss cmin l _ =+ all (\pos -> chessDist lxsize l pos > cmin) poss+ tryFind _ 0 = return []+ tryFind ps n = do+ np <- findPosTry 40 ltile+ [ dist ps factionDist+ , dist ps $ 2 * factionDist `div` 3+ , dist ps $ factionDist `div` 2+ , dist ps $ factionDist `div` 3+ , dist ps $ factionDist `div` 4+ , dist ps $ factionDist `div` 6+ , const (Tile.hasFeature cotile F.CanActor)+ ]+ nps <- tryFind (np : ps) (n - 1)+ return $ np : nps+ stairPoss | k == 1 = [fst lstair, snd lstair]+ | otherwise = []+ in tryFind stairPoss k
+ Game/LambdaHack/Server/State.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Server and client game state types and operations.+module Game.LambdaHack.Server.State+ ( StateServer(..), emptyStateServer+ , DebugModeSer(..), defDebugModeSer+ ) where++import Data.Binary+import qualified Data.EnumMap.Strict as EM+import qualified Data.HashMap.Strict as HM+import Data.Text (Text)+import qualified System.Random as R++import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.AtomicCmd+import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.Perception+import Game.LambdaHack.Server.Config+import Game.LambdaHack.Server.Fov++-- | 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+ , scenario :: !Text -- ^ current game mode+ , sconfig :: Config -- ^ this game's config (including initial RNG)+ , squit :: !Bool -- ^ exit the game loop+ , sbkpSave :: !Bool -- ^ make backup savefile now+ , sdebugSer :: !DebugModeSer -- ^ current debugging mode+ , sdebugNxt :: !DebugModeSer -- ^ debugging mode for the next game+ }+ deriving (Show)++data DebugModeSer = DebugModeSer+ { sknowMap :: !Bool+ , sknowEvents :: !Bool+ , sniffIn :: !Bool+ , sniffOut :: !Bool+ , sallClear :: !Bool+ , stryFov :: !(Maybe FovMode)+ , sdebugCli :: !Bool+ }+ deriving Show++-- | Initial, empty game server state.+emptyStateServer :: StateServer+emptyStateServer =+ StateServer+ { sdisco = EM.empty+ , sdiscoRev = EM.empty+ , sitemRev = HM.empty+ , sflavour = emptyFlavourMap+ , sacounter = toEnum 0+ , sicounter = toEnum 0+ , sundo = []+ , sper = EM.empty+ , srandom = R.mkStdGen 42+ , scenario = "campaign"+ , sconfig = undefined+ , squit = False+ , sbkpSave = False+ , sdebugSer = defDebugModeSer+ , sdebugNxt = defDebugModeSer+ }++defDebugModeSer :: DebugModeSer+defDebugModeSer = DebugModeSer { sknowMap = False+ , sknowEvents = False+ , sniffIn = False+ , sniffOut = False+ , sallClear = False+ , stryFov = Nothing+ , sdebugCli = False+ }++instance Binary StateServer where+ put StateServer{..} = do+ put sdisco+ put sdiscoRev+ put sitemRev+ put sflavour+ put sacounter+ put sicounter+ put sundo+ put (show srandom)+ put scenario+ put sconfig+ put sdebugSer+ get = do+ sdisco <- get+ sdiscoRev <- get+ sitemRev <- get+ sflavour <- get+ sacounter <- get+ sicounter <- get+ sundo <- get+ g <- get+ scenario <- get+ sconfig <- get+ sdebugSer <- get+ let srandom = read g+ sper = EM.empty+ squit = False+ sbkpSave = False+ sdebugNxt = defDebugModeSer+ return StateServer{..}++instance Binary DebugModeSer where+ put DebugModeSer{..} = do+ put sknowMap+ put sknowEvents+ put sniffIn+ put sniffOut+ put sallClear+ put stryFov+ put sdebugCli+ get = do+ sknowMap <- get+ sknowEvents <- get+ sniffIn <- get+ sniffOut <- get+ sallClear <- get+ stryFov <- get+ sdebugCli <- get+ return DebugModeSer{..}
− Game/LambdaHack/State.hs
@@ -1,322 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Game state and persistent player diary types and operations.-module Game.LambdaHack.State- ( -- * Game state- State(..), TgtMode(..), Cursor(..), Status(..)- -- * Accessor- , slevel, stime- -- * Constructor- , defaultState- -- * State update- , updateCursor, updateTime, updateDiscoveries, updateLevel, updateDungeon- -- * Player diary- , Diary(..), defaultDiary- -- * Textia; descriptions- , lookAt, partItemCheat, partItem, partItemNWs- -- * Debug flags- , DebugMode(..), cycleMarkVision, toggleOmniscient- ) where--import qualified Data.Set as S-import Data.Binary-import qualified System.Random as R-import System.Time-import Data.Text (Text)-import qualified Data.Text as T-import qualified NLP.Miniutter.English as MU-import qualified Data.List as L--import Game.LambdaHack.Actor-import Game.LambdaHack.Point-import Game.LambdaHack.Level-import qualified Game.LambdaHack.Dungeon as Dungeon-import Game.LambdaHack.Item-import Game.LambdaHack.Msg-import Game.LambdaHack.Time-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Content.FactionKind-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Effect-import Game.LambdaHack.Flavour-import Game.LambdaHack.Config---- | The diary contains all the player data that carries over--- from game to game, even across playing sessions. That includes--- the last message, previous messages and otherwise recorded--- history of past games. This can be extended with other data and used for--- calculating player achievements, unlocking advanced game features and--- for general data mining, e.g., augmenting AI or procedural content--- generation.-data Diary = Diary- { sreport :: Report- , shistory :: History- }---- TODO: stakeTime and squit are also temporary, move them to--- DungeonPerception and rename it to TurnCache, if more appear, e.g. AI stuff.--- | The state of a single game that can be saved and restored.--- It's completely disregarded and reset when a new game is started.--- In practice, we maintain some extra state (DungeonPerception),--- but it's only temporary, existing for a single turn and then invalidated.-data State = State- { splayer :: ActorId -- ^ represents the player-controlled actor- , scursor :: Cursor -- ^ cursor location and level to return to- , sflavour :: FlavourMap -- ^ association of flavour to items- , sdisco :: Discoveries -- ^ items (kinds) that have been discovered- , sdungeon :: Dungeon.Dungeon -- ^ all dungeon levels- , slid :: Dungeon.LevelId -- ^ identifier of the current level- , scounter :: Int -- ^ stores next actor index- , srandom :: R.StdGen -- ^ current random generator- , sconfig :: Config -- ^ this game's config (including initial RNG)- , stakeTime :: Maybe Bool -- ^ last command unexpectedly took some time- , squit :: Maybe (Bool, Status) -- ^ cause of game end/exit- , sfaction :: Kind.Id FactionKind -- ^ our faction- , sdebug :: DebugMode -- ^ debugging mode- }- deriving Show---- | Current targeting mode of the player.-data TgtMode =- TgtOff -- ^ not in targeting mode- | TgtExplicit -- ^ the player requested targeting mode explicitly- | TgtAuto -- ^ the mode was entered (and will be exited) automatically- deriving (Show, Eq)---- | Current targeting cursor parameters.-data Cursor = Cursor- { ctargeting :: TgtMode -- ^ targeting mode- , clocLn :: Dungeon.LevelId -- ^ cursor level- , clocation :: Point -- ^ cursor coordinates- , creturnLn :: Dungeon.LevelId -- ^ the level current player resides on- , ceps :: Int -- ^ a parameter of the tgt digital line- }- deriving Show---- | Current result of the game.-data Status =- Killed !Dungeon.LevelId -- ^ the player lost the game on the given level- | Camping -- ^ game is supended- | Victor -- ^ the player won- | Restart -- ^ the player quits and starts a new game- deriving (Show, Eq, Ord)--data DebugMode = DebugMode- { smarkVision :: Maybe FovMode- , somniscient :: Bool- }- deriving Show---- | Get current level from the dungeon data.-slevel :: State -> Level-slevel State{slid, sdungeon} = sdungeon Dungeon.! slid---- | Get current time from the dungeon data.-stime :: State -> Time-stime State{slid, sdungeon} = ltime $ sdungeon Dungeon.! slid---- | Initial player diary.-defaultDiary :: IO Diary-defaultDiary = do- dateTime <- getClockTime- let curDate = MU.Text $ T.pack $ calendarTimeToString $ toUTCTime dateTime- return Diary- { sreport = emptyReport- , shistory = singletonHistory $ singletonReport $- makeSentence ["Player diary started on", curDate]- }---- | Initial game state.-defaultState :: Config -> Kind.Id FactionKind -> FlavourMap- -> Dungeon.Dungeon -> Dungeon.LevelId -> Point -> R.StdGen- -> State-defaultState config sfaction flavour dng lid ploc g =- State- 0 -- hack: the hero is not yet alive- (Cursor TgtOff lid ploc lid 0)- flavour- S.empty- dng- lid- 0- g- config- Nothing- Nothing- sfaction- defaultDebugMode--defaultDebugMode :: DebugMode-defaultDebugMode = DebugMode- { smarkVision = Nothing- , somniscient = False- }---- | Update cursor parameters within state.-updateCursor :: (Cursor -> Cursor) -> State -> State-updateCursor f s = s { scursor = f (scursor s) }---- | Update time within state.-updateTime :: (Time -> Time) -> State -> State-updateTime f s = updateLevel (\ lvl@Level{ltime} -> lvl {ltime = f ltime}) s---- | Update item discoveries within state.-updateDiscoveries :: (Discoveries -> Discoveries) -> State -> State-updateDiscoveries f s = s { sdisco = f (sdisco s) }---- | Update level data within state.-updateLevel :: (Level -> Level) -> State -> State-updateLevel f s = updateDungeon (Dungeon.adjust f (slid s)) s---- | Update dungeon data within state.-updateDungeon :: (Dungeon.Dungeon -> Dungeon.Dungeon) -> State -> State-updateDungeon f s = s {sdungeon = f (sdungeon s)}--cycleMarkVision :: State -> State-cycleMarkVision s@State{sdebug = sdebug@DebugMode{smarkVision}} =- s {sdebug = sdebug {smarkVision = case smarkVision of- Nothing -> Just (Digital 100)- Just (Digital _) -> Just Permissive- Just Permissive -> Just Shadow- Just Shadow -> Just Blind- Just Blind -> Nothing }}--toggleOmniscient :: State -> State-toggleOmniscient s@State{sdebug = sdebug@DebugMode{somniscient}} =- s {sdebug = sdebug {somniscient = not somniscient}}--instance Binary Diary where- put Diary{..} = do- put sreport- put shistory- get = do- sreport <- get- shistory <- get- return Diary{..}--instance Binary State where- put (State player cursor flav disco dng lid ct- g config stakeTime _ sfaction _) = do- put player- put cursor- put flav- put disco- put dng- put lid- put ct- put (show g)- put config- put stakeTime- put sfaction- get = do- player <- get- cursor <- get- flav <- get- disco <- get- dng <- get- lid <- get- ct <- get- g <- get- config <- get- stakeTime <- get- sfaction <- get- return- (State player cursor flav disco dng lid ct (read g) config stakeTime- Nothing sfaction defaultDebugMode)--instance Binary TgtMode where- put TgtOff = putWord8 0- put TgtExplicit = putWord8 1- put TgtAuto = putWord8 2- get = do- tag <- getWord8- case tag of- 0 -> return TgtOff- 1 -> return TgtExplicit- 2 -> return TgtAuto- _ -> fail "no parse (TgtMode)"--instance Binary Cursor where- put (Cursor act cln loc rln eps) = do- put act- put cln- put loc- put rln- put eps- get = do- act <- get- cln <- get- loc <- get- rln <- get- eps <- get- return (Cursor act cln loc rln eps)--instance Binary Status where- put (Killed ln) = putWord8 0 >> put ln- put Camping = putWord8 1- put Victor = putWord8 2- put Restart = putWord8 3- get = do- tag <- getWord8- case tag of- 0 -> fmap Killed get- 1 -> return Camping- 2 -> return Victor- 3 -> return Restart- _ -> fail "no parse (Status)"---- TODO: probably move these somewhere---- | The part of speech describing the item.--- If cheating is allowed, full identity of the item is revealed--- together with its flavour (e.g. at the game over screen).-partItemCheat :: Bool -> Kind.Ops ItemKind -> State -> Item -> MU.Part-partItemCheat cheat coitem@Kind.Ops{okind} state i =- let ik = jkind i- kind = okind ik- identified = L.length (iflavour kind) == 1 ||- ik `S.member` sdisco state- eff = effectToSuffix (ieffect kind)- pwr = if jpower i == 0- then ""- else "(+" <> showT (jpower i) <> ")"- genericName = iname kind- name = let fullName = genericName <+> eff <+> pwr- flavour = getFlavour coitem (sflavour state) ik- in if identified- then fullName- else flavourToName flavour- <+> if cheat then fullName else genericName- in MU.Text name---- | The part of speech describing the item.-partItem :: Kind.Ops ItemKind -> State -> Item -> MU.Part-partItem = partItemCheat False--partItemNWs :: Kind.Ops ItemKind -> State -> Item -> MU.Part-partItemNWs coitem s i = MU.NWs (jcount i) $ partItem coitem s i---- | Produces a textual description of the terrain and items at an already--- explored location. Mute for unknown locations.--- The detailed variant is for use in the targeting mode.-lookAt :: Kind.COps -- ^ game content- -> Bool -- ^ detailed?- -> Bool -- ^ can be seen right now?- -> State -- ^ game state- -> Level -- ^ current level- -> Point -- ^ location to describe- -> Text -- ^ an extra sentence to print- -> Text-lookAt Kind.COps{coitem, cotile=Kind.Ops{oname}} detailed canSee s lvl loc msg- | detailed =- let tile = lvl `rememberAt` loc- in makeSentence [MU.Text $ oname tile] <+> msg <+> isd- | otherwise = msg <+> isd- where- is = lvl `rememberAtI` loc- prefixSee = MU.Text $ if canSee then "you see" else "you remember"- isd = case is of- [] -> ""- _ | length is <= 3 ->- makeSentence [prefixSee, MU.WWandW $ map (partItemNWs coitem s) is]- _ | detailed -> "Objects:"- _ -> "Objects here."
− Game/LambdaHack/Strategy.hs
@@ -1,84 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | AI strategies to direct actors not controlled by the player.--- No operation in this module involves the 'State' or 'Action' type.-module Game.LambdaHack.Strategy- ( Strategy, nullStrategy, liftFrequency- , (.|), reject, (.=>), only, bestVariant, renameStrategy, returN- ) where--import Control.Monad-import Data.Text (Text)--import Game.LambdaHack.Utils.Frequency-import Game.LambdaHack.Msg---- | A strategy is a choice of (non-empty) frequency tables--- of possible actions.-newtype Strategy a = Strategy { runStrategy :: [Frequency a] }- deriving Show---- | 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]- m >>= f = normalizeStrategy $ Strategy $- [ toFreq name [ (p * q, b)- | (p, a) <- runFrequency x- , y <- runStrategy (f a)- , (q, b) <- runFrequency y- ]- | x <- runStrategy m- , let name = "Strategy_bind (" <> nameFrequency x <> ")"]--instance MonadPlus Strategy where- mzero = Strategy []- mplus (Strategy xs) (Strategy ys) = Strategy (xs ++ ys)--normalizeStrategy :: Strategy a -> Strategy a-normalizeStrategy (Strategy fs) = Strategy $ filter (not . nullFreq) fs--nullStrategy :: Strategy a -> Bool-nullStrategy strat = null $ runStrategy strat---- | Strategy where only the actions from the given single frequency table--- can be picked.-liftFrequency :: Frequency a -> Strategy a-liftFrequency f = normalizeStrategy $ Strategy $ return f--infixr 2 .|---- | Strategy with the actions from both argument strategies,--- with original frequencies.-(.|) :: Strategy a -> Strategy a -> Strategy a-(.|) = mplus---- | Strategy with no actions at all.-reject :: Strategy a-reject = mzero--infix 3 .=>---- | Conditionally accepted strategy.-(.=>) :: Bool -> Strategy a -> Strategy a-p .=> m | p = m- | otherwise = mzero---- | Strategy with all actions not satisfying the predicate removed.--- The remaining actions keep their original relative frequency values.-only :: (a -> Bool) -> Strategy a -> Strategy a-only p s = normalizeStrategy $ do- x <- s- p x .=> return x---- | When better choices are towards the start of the list,--- this is the best frequency of the strategy.-bestVariant :: Strategy a -> Frequency a-bestVariant (Strategy []) = mzero-bestVariant (Strategy (f : _)) = f---- | Overwrite the description of all frequencies within the strategy.-renameStrategy :: Text -> Strategy a -> Strategy a-renameStrategy newName (Strategy fs) = Strategy $ map (renameFreq newName) fs---- | Like 'return', but pick a name of the single frequency.-returN :: Text -> a -> Strategy a-returN name x = Strategy $ return $ uniformFreq name [x]
− Game/LambdaHack/StrategyAction.hs
@@ -1,390 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | AI strategy operations implemented with the 'Action' monad.-module Game.LambdaHack.StrategyAction- ( targetStrategy, strategy- ) where--import qualified Data.List as L-import qualified Data.IntMap as IM-import Data.Maybe-import Data.Function-import Control.Monad-import Control.Monad.State hiding (State, state)-import Control.Arrow-import qualified Data.Text as T--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Ability (Ability)-import qualified Game.LambdaHack.Ability as Ability-import Game.LambdaHack.Point-import Game.LambdaHack.Vector-import Game.LambdaHack.Level-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import Game.LambdaHack.Content.ActorKind-import Game.LambdaHack.Utils.Frequency-import Game.LambdaHack.Perception-import Game.LambdaHack.Strategy-import Game.LambdaHack.State-import Game.LambdaHack.Action-import Game.LambdaHack.Msg-import Game.LambdaHack.EffectAction-import Game.LambdaHack.Actions-import Game.LambdaHack.ItemAction-import Game.LambdaHack.Content.ItemKind-import Game.LambdaHack.Content.RuleKind-import Game.LambdaHack.Item-import qualified Game.LambdaHack.Effect as Effect-import qualified Game.LambdaHack.Tile as Tile-import qualified Game.LambdaHack.Kind as Kind-import qualified Game.LambdaHack.Feature as F-import Game.LambdaHack.Time-import qualified Game.LambdaHack.Color as Color---- | AI proposes possible targets for the actor. Never empty.-targetStrategy :: Kind.COps -> ActorId -> State -> Perception -> [Ability]- -> Strategy Target-targetStrategy cops actor state@State{splayer = pl} per factionAbilities =- retarget btarget- where- Kind.COps{ cotile- , coactor=coactor@Kind.Ops{okind}- } = cops- lvl@Level{lxsize} = slevel state- actorBody@Actor{ bkind, bloc = me, btarget, bfaction } =- getActor actor state- mk = okind bkind- enemyVisible a l =- asight mk- && actorSeesActor cotile per lvl actor a me l pl- -- Enemy can be felt if adjacent (e. g., a player-controlled monster).- -- TODO: can this be replaced by setting 'lights' to [me]?- || adjacent lxsize me l- && (asmell mk || asight mk)- actorAbilities = acanDo (okind bkind) `L.intersect` factionAbilities- focused = actorSpeed coactor actorBody <= 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- retarget :: Target -> Strategy Target- retarget tgt =- case tgt of- TPath _ -> returN "TPath" tgt -- don't animate missiles- TEnemy a ll | focused- && memActor a state -- present on this level- -- Don't hit a new player-controlled monster.- && not (isAHero state actor && a == pl) ->- let l = bloc $ getActor a state- in if enemyVisible a l -- prefer visible foes- then returN "TEnemy" $ TEnemy a l- else if null visibleFoes -- prefer visible foes- && me /= ll -- not yet reached the last enemy loc- then returN "last known" $ TLoc ll- -- chase the last known loc- else closest- TEnemy _ _ -> closest -- foe is gone and we forget- TLoc loc | me == loc -> closest -- already reached the loc- TLoc _ | null visibleFoes -> returN "TLoc" tgt- -- nothing visible, go to loc- TLoc _ -> closest -- prefer visible foes- TCursor -> closest- hs = hostileAssocs bfaction lvl- foes = if isAHero state actor- then L.filter ((pl /=) . fst) hs -- ignore player-controlled- else if not (isAHero state pl) && memActor pl state- then (pl, getPlayerBody state) : hs- else hs -- no player-controlled monster to add- visibleFoes = L.filter (uncurry enemyVisible) (L.map (second bloc) foes)- closest :: Strategy Target- closest =- let foeDist = L.map (\ (_, l) -> chessDist lxsize me l) visibleFoes- minDist = L.minimum foeDist- minFoes =- L.filter (\ (_, l) -> chessDist lxsize me l == minDist) visibleFoes- minTargets = map (\ (a, l) -> TEnemy a l) minFoes- minTgtS = liftFrequency $ uniformFreq "closest" minTargets- in minTgtS .| noFoes .| returN "TCursor" TCursor -- never empty- -- TODO: set distant targets so that monsters behave as if they have- -- a plan. We need pathfinding for that.- noFoes :: Strategy Target- noFoes =- (TLoc . (me `shift`)) `liftM` moveStrategy cops actor state Nothing---- | AI strategy based on actor's sight, smell, intelligence, etc. Never empty.-strategy :: Kind.COps -> ActorId -> State -> [Ability] -> Strategy (Action ())-strategy cops actor state factionAbilities =- sumS prefix .| combineDistant distant .| sumS suffix- .| waitBlockNow actor -- wait until friends sidestep, ensures never empty- where- Kind.COps{coactor=Kind.Ops{okind}} = cops- Actor{ bkind, bloc, btarget } = getActor actor state- (floc, foeVisible) = case btarget of- TEnemy _ l -> (l, True)- TLoc l -> (l, False)- TPath _ -> (bloc, False) -- a missile- TCursor -> (bloc, False) -- an actor blocked by friends- combineDistant = liftFrequency . sumF- aFrequency :: Ability -> Frequency (Action ())- aFrequency Ability.Ranged = if foeVisible- then rangedFreq cops actor state floc- else mzero- aFrequency Ability.Tools = if foeVisible- then toolsFreq cops actor state- else mzero- aFrequency Ability.Chase = if (floc /= bloc)- then chaseFreq- else mzero- aFrequency _ = assert `failure` distant- chaseFreq =- scaleFreq 30 $ bestVariant $ chase cops actor state (floc, foeVisible)- aStrategy :: Ability -> Strategy (Action ())- aStrategy Ability.Track = track cops actor state- aStrategy Ability.Heal = mzero -- TODO- aStrategy Ability.Flee = mzero -- TODO- aStrategy Ability.Melee = foeVisible .=> melee actor state floc- aStrategy Ability.Pickup = not foeVisible .=> pickup actor state- aStrategy Ability.Wander = wander cops actor state- aStrategy _ = assert `failure` actorAbilities- actorAbilities = acanDo (okind bkind) `L.intersect` factionAbilities- isDistant = (`elem` [Ability.Ranged, Ability.Tools, Ability.Chase])- (prefix, rest) = L.break isDistant actorAbilities- (distant, suffix) = L.partition isDistant rest- sumS = msum . map aStrategy- sumF = msum . map aFrequency--dirToAction :: ActorId -> Bool -> Vector -> Action ()-dirToAction actor allowAttacks dir = do- -- set new direction- updateAnyActor actor $ \ m -> m { bdir = Just (dir, 0) }- -- perform action- tryWith (\ msg -> if T.null msg- then return ()- else assert `failure` msg <> "in AI") $ do- -- If the following action aborts, we just advance the time and continue.- -- TODO: ensure time is taken for other aborted actions in this file- -- TODO: or just fail at each abort in AI code? or use tryWithFrame?- moveOrAttack allowAttacks actor dir---- | A strategy to always just wait.-waitBlockNow :: ActorId -> Strategy (Action ())-waitBlockNow actor = returN "wait" $ setWaitBlock actor---- | A strategy to always just die.-dieNow :: ActorId -> Strategy (Action ())-dieNow actor = returN "die" $ do -- TODO: explode if a potion- bitems <- gets (getActorItem actor)- Actor{bloc} <- gets (getActor actor)- modify (updateLevel (dropItemsAt bitems bloc))- modify (deleteActor actor)---- | Strategy for dumb missiles.-track :: Kind.COps -> ActorId -> State -> Strategy (Action ())-track cops actor state =- strat- where- lvl = slevel state- Actor{ bloc, btarget, bhp } = getActor actor state- darkenActor = updateAnyActor actor $ \ m -> m {bcolor = Just Color.BrBlack}- dieOrReset | bhp <= 0 = dieNow actor- | otherwise =- returN "reset TPath" $ updateAnyActor actor- $ \ m -> m {btarget = TCursor}- strat = case btarget of- TPath [] -> dieOrReset- TPath (d : _) | not $ accessible cops lvl bloc (shift bloc d) -> dieOrReset- -- TODO: perhaps colour differently the whole second turn of movement?- TPath [d] -> returN "last TPath" $ do- darkenActor- updateAnyActor actor $ \ m -> m { btarget = TPath [] }- dirToAction actor True d- TPath (d : lv) -> returN "follow TPath" $ do- updateAnyActor actor $ \ m -> m { btarget = TPath lv }- dirToAction actor True d- _ -> reject--pickup :: ActorId -> State -> Strategy (Action ())-pickup actor state =- lootHere bloc .=> actionPickup- where- lvl = slevel state- Actor{bloc} = getActor actor state- lootHere x = not $ L.null $ lvl `atI` x- actionPickup = returN "pickup" $ actorPickupItem actor--melee :: ActorId -> State -> Point -> Strategy (Action ())-melee actor state floc =- foeAdjacent .=> (returN "melee" $ dirToAction actor True dir)- where- Level{lxsize} = slevel state- Actor{bloc} = getActor actor state- foeAdjacent = adjacent lxsize bloc floc- dir = displacement bloc floc--rangedFreq :: Kind.COps -> ActorId -> State -> Point -> Frequency (Action ())-rangedFreq cops actor state@State{splayer = pl} floc =- toFreq "throwFreq" $- if not foesAdj- && asight mk- && accessible cops lvl bloc loc1 -- first accessible- && isNothing (locToActor loc1 state) -- no friends on first- then throwFreq bitems 3 ++ throwFreq tis 6- else []- where- Kind.COps{ coactor=Kind.Ops{okind}- , coitem=Kind.Ops{okind=iokind}- , corule- } = cops- lvl@Level{lxsize, lysize} = slevel state- Actor{ bkind, bloc, bfaction } = getActor actor state- bitems = getActorItem actor state- mk = okind bkind- tis = lvl `atI` bloc- hs = hostileAssocs bfaction lvl- foes = if isAHero state actor- then L.filter ((pl /=) . fst) hs -- ignore player-controlled- else if not (isAHero state pl) && memActor pl state- then (pl, getPlayerBody state) : hs- else hs -- no player-controlled monster to add- foesAdj = foesAdjacent lxsize lysize bloc (map snd foes)- -- TODO: also don't throw if any loc 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.- eps = 0- bl = bla lxsize lysize eps bloc floc -- TODO:make an arg of projectGroupItem- loc1 = case bl of- Nothing -> bloc -- TODO- Just [] -> bloc -- TODO- Just (lbl:_) -> lbl- throwFreq is multi =- [ (benefit * multi,- projectGroupItem actor floc (iverbProject ik) i)- | i <- is,- let ik = iokind (jkind i),- let benefit = - (1 + jpower i) * Effect.effectToBenefit (ieffect ik),- benefit > 0,- -- Wasting weapons and armour would be too cruel to the player.- isymbol ik `elem` (ritemProject $ Kind.stdRuleset corule)]--toolsFreq :: Kind.COps -> ActorId -> State -> Frequency (Action ())-toolsFreq cops actor state =- toFreq "quaffFreq" $ quaffFreq bitems 1 ++ quaffFreq tis 2- where- Kind.COps{coitem=Kind.Ops{okind=iokind}} = cops- lvl = slevel state- Actor{bloc} = getActor actor state- bitems = getActorItem actor state- tis = lvl `atI` bloc- quaffFreq is multi =- [ (benefit * multi, applyGroupItem actor (iverbApply ik) i)- | i <- is,- let ik = iokind (jkind i),- let benefit = (1 + jpower i) * Effect.effectToBenefit (ieffect ik),- benefit > 0, isymbol ik == '!']---- | 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 :: Kind.COps -> ActorId -> State -> Maybe (Point, Bool)- -> Strategy Vector-moveStrategy cops actor state mFoe =- case mFoe of- -- Target set and we chase the foe or his last position or another target.- Just (floc, foeVisible) ->- let towardsFoe =- let foeDir = towards lxsize bloc floc- tolerance | isUnit lxsize foeDir = 0- | otherwise = 1- in only (\ x -> euclidDistSq lxsize foeDir x <= tolerance)- in if floc == bloc- 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)- $ L.groupBy ((==) `on` snd)- $ L.sortBy (flip compare `on` snd)- $ L.filter (\ (_, s) -> s > timeZero)- $ L.map (\ x ->- let sml = IM.findWithDefault- timeZero (bloc `shift` x) lsmell- in (x, sml `timeAdd` timeNegate ltime))- sensible- in asmell mk .=> L.foldr ((.|)- . liftFrequency- . uniformFreq "smell k") reject smells- .| moveOpenable -- no enemy in sight, explore doors- .| moveClear- where- Kind.COps{ cotile- , coactor=Kind.Ops{okind}- , coitem- } = cops- lvl@Level{lsmell, lxsize, lysize, ltime} = slevel state- Actor{ bkind, bloc, bdir, bfaction } = getActor actor state- bitems = getActorItem actor state- mk = okind bkind- lootHere x = not $ L.null $ lvl `atI` x- onlyLoot = onlyMoves lootHere bloc- interestHere x = let t = lvl `at` x- ts = map (lvl `at`) $ vicinity lxsize lysize x- in Tile.hasFeature cotile F.Exit t- -- Lit indirectly. E.g., a room entrance.- || (not (Tile.hasFeature cotile F.Lit t)- && L.any (Tile.hasFeature cotile F.Lit) ts)- onlyInterest = onlyMoves interestHere bloc- onlyKeepsDir k =- only (\ x -> maybe True (\ (d, _) -> euclidDistSq lxsize d x <= k) bdir)- onlyKeepsDir_9 = only (\ x -> maybe True (\ (d, _) -> neg x /= d) bdir)- moveIQ = aiq mk > 15 .=> onlyKeepsDir 0 moveRandomly- .| aiq mk > 10 .=> onlyKeepsDir 1 moveRandomly- .| aiq mk > 5 .=> onlyKeepsDir 2 moveRandomly- .| onlyKeepsDir_9 moveRandomly- interestFreq | interestHere bloc =- -- Don't detour towards an interest if already on one.- mzero- | otherwise =- -- Prefer interests, but don't exclude other focused moves.- scaleFreq 5 $ bestVariant $ onlyInterest $ onlyKeepsDir 2 moveRandomly- interestIQFreq = interestFreq `mplus` bestVariant moveIQ- moveClear = onlyMoves (not . openableHere) bloc moveFreely- moveOpenable = onlyMoves openableHere bloc moveFreely- moveFreely = onlyLoot moveRandomly- .| liftFrequency interestIQFreq- .| moveIQ -- sometimes interestIQFreq is excluded later on- .| moveRandomly- onlyMoves :: (Point -> Bool) -> Point -> Strategy Vector -> Strategy Vector- onlyMoves p l = only (\ x -> p (l `shift` x))- moveRandomly :: Strategy Vector- moveRandomly = liftFrequency $ uniformFreq "moveRandomly" sensible- -- Monsters don't see doors more secret than that. Enforced when actually- -- opening doors, too, so that monsters don't cheat. TODO: remove the code- -- duplication, though. TODO: make symmetric for playable monster faction?- openPower = timeScale timeTurn $- case strongestSearch coitem bitems of- Just i -> aiq mk + jpower i- Nothing -> aiq mk- openableHere = openable cotile lvl openPower- accessibleHere = accessible cops lvl bloc- noFriends | asight mk = unoccupied (factionList [bfaction] state)- | otherwise = const True- isSensible l = noFriends l && (accessibleHere l || openableHere l)- sensible = filter (isSensible . (bloc `shift`)) (moves lxsize)--chase :: Kind.COps -> ActorId -> State -> (Point, Bool) -> Strategy (Action ())-chase cops actor state foe@(_, foeVisible) =- -- 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- in dirToAction actor fight `liftM` moveStrategy cops actor state mFoe--wander :: Kind.COps -> ActorId -> State -> Strategy (Action ())-wander cops actor state =- -- Target set, but we don't chase the foe, e.g., because we are blocked- -- or we cannot chase at all.- let mFoe = Nothing- in dirToAction actor True `liftM` moveStrategy cops actor state mFoe
− Game/LambdaHack/Tile.hs
@@ -1,96 +0,0 @@--- | Operations concerning dungeon level tiles.------ Unlike for many other content types, there is no type @Tile@,--- of particular concrete tiles in the dungeon,--- corresponding to 'TileKind' (the type of kinds of terrain tiles).--- This is because the tiles are too numerous and there's not enough--- storage space for a well-rounded @Tile@ type, on one hand,--- and on the other hand, tiles are accessed--- too often in performance critical code--- to try to compress their representation and/or recompute them.--- Instead, of defining a @Tile@ type, we express various properties--- of concrete tiles by arrays or sparse IntMaps, as appropriate.------ Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m).-module Game.LambdaHack.Tile- (SecretTime, SmellTime- , kindHasFeature, kindHas, hasFeature- , isClear, isLit, isExplorable, similar, canBeHidden, speedup- ) where--import qualified Data.List as L-import qualified Data.Array.Unboxed as A--import Game.LambdaHack.Content.TileKind-import qualified Game.LambdaHack.Feature as F-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Time---- | The time interval needed to discover a given secret,--- e.g., a hidden terrain tile, e.g., a hidden door.-type SecretTime = Time---- | The last time a hero left a smell in a given tile. To be used--- by monsters that hunt by smell.-type SmellTime = Time---- | Whether a tile kind has the given feature.-kindHasFeature :: F.Feature -> TileKind -> Bool-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)---- | 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 = Kind.TileSpeedup{isClearTab}} = isClearTab---- | 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 = Kind.TileSpeedup{isLitTab}} = isLitTab---- | Whether a tile can be explored, possibly yielding a treasure--- or a hidden message. We exclude doors and hidden features--- (TODO: and features created by actors, e.g., dug out).-isExplorable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool-isExplorable cops tk =- not (hasFeature cops F.Closable tk)- && (isClear cops tk- || hasFeature cops F.Walkable tk)---- | The player can't tell one tile from the other.-similar :: TileKind -> TileKind -> Bool-similar t u =- tsymbol t == tsymbol u &&- tname t == tname u &&- tcolor t == tcolor u &&- tcolor2 t == tcolor2 u---- | The player can't tell if the tile is hidden or not.-canBeHidden :: Kind.Ops TileKind -> TileKind -> Bool-canBeHidden Kind.Ops{ofoldrWithKey} t =- let sim _ s acc = acc || kindHasFeature F.Hidden s && similar t s- in ofoldrWithKey sim False--speedup :: Kind.Ops TileKind -> Kind.Speedup TileKind-speedup 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 = tabulate $ kindHasFeature F.Clear- isLitTab = tabulate $ kindHasFeature F.Lit- in Kind.TileSpeedup {isClearTab, isLitTab}
− Game/LambdaHack/Time.hs
@@ -1,143 +0,0 @@--- | Game time and speed.-module Game.LambdaHack.Time- ( Time, timeZero, timeClip, timeTurn- , timeAdd, timeFit, timeNegate, timeScale- , timeToDigit- , Speed, toSpeed, speedNormal- , speedScale, ticksPerMeter, traveled, speedFromWeight, rangeFromSpeed- ) where--import Data.Binary-import Data.Int (Int64)-import qualified Data.Char as Char---- | Game time in ticks. The time dimension.--- One tick is 1 microsecond (one millionth of a second),--- one turn is 0.5 s.-newtype Time = Time Int64- deriving (Show, Eq, Ord)--instance Binary Time where- put (Time n) = put n- get = fmap Time get---- | Start of the game time, or zero lenght time interval.-timeZero :: Time-timeZero = Time 0---- | The smallest unit of time. Do not export, because the proportion--- of turn to tick is an implementation detail.--- The significance of this detail is only that it determines resolution--- of the time dimension.-_timeTick :: Time-_timeTick = Time 1---- TODO: don't have a fixed time, but instead set it at 1/3 or 1/4--- of timeTurn depending on level. Clips are a UI feature--- after all, so should depend on the user situation.--- | At least once per clip all moves are resolved and a frame--- or a frame delay is generated.--- Currently one clip is 0.1 s, but it may change,--- and the code should not depend on this fixed value.-timeClip :: Time-timeClip = Time 100000---- | One turn is 0.5 s. The code may depend on that.--- Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m).-timeTurn :: Time-timeTurn = Time 500000---- | This many turns fit in a single second.-turnsInSecond :: Int64-turnsInSecond = 2---- | This many ticks fits in a single second. Do not export,-_ticksInSecond :: Int64-_ticksInSecond =- let Time ticksInTurn = timeTurn- in ticksInTurn * turnsInSecond---- | Time addition.-timeAdd :: Time -> Time -> Time-timeAdd (Time t1) (Time t2) = Time (t1 + t2)---- | How many time intervals of the latter kind fits in an interval--- of the former kind.-timeFit :: Time -> Time -> Int-timeFit (Time t1) (Time t2) = fromIntegral $ t1 `div` t2---- | Negate a time interval. Can be used to subtract from a time--- or to reverse the ordering on time.-timeNegate :: Time -> Time-timeNegate (Time t) = Time (-t)---- | Scale time by an @Int@ scalar value.-timeScale :: Time -> Int -> Time-timeScale (Time t) s = Time (t * fromIntegral s)---- | Represent the main 10 thresholds of a time range by digits,--- given the total length of the time range.-timeToDigit :: Time -> Time -> Char-timeToDigit (Time maxT) (Time t) =- let k = 10 * t `div` maxT- digit | k > 9 = '*'- | k < 0 = '-'- | otherwise = Char.intToDigit $ fromIntegral k- in digit---- | Speed in meters per 1 million seconds (m/Ms).--- Actors at normal speed (2 m/s) take one time turn (0.5 s)--- to move one tile (1 m by 1 m).-newtype Speed = Speed Int64- deriving (Show, Eq, Ord)--instance Binary Speed where- put (Speed n) = put n- get = fmap Speed get---- | Number of seconds in a kilo-second.-sInMs :: Int64-sInMs = 1000000---- | Constructor for content definitions.-toSpeed :: Double -> Speed-toSpeed s = Speed $ round $ s * fromIntegral sInMs---- | Normal speed (2 m/s) that suffices to move one tile in one turn.-speedNormal :: Speed-speedNormal = Speed $ 2 * sInMs---- | Scale speed by an @Int@ scalar value.-speedScale :: Speed -> Int -> Speed-speedScale (Speed v) s = Speed (v * fromIntegral s)---- | 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)---- | Calculate projectile speed from item weight in grams--- and speed bonus in percents.--- See <https://github.com/kosmikus/LambdaHack/wiki/Item-statistics>.-speedFromWeight :: Int -> Int -> Speed-speedFromWeight weight bonus =- let w = fromIntegral weight- b = fromIntegral bonus- 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---- | 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,--- that is 2 turns, and then drops to the ground.--- Dividing and multiplying by 2 ensures both turns of flight--- cover the same distance.-rangeFromSpeed :: Speed -> Int-rangeFromSpeed (Speed v) = fromIntegral $ 2 * (v `div` (sInMs * 2))
− Game/LambdaHack/Turn.hs
@@ -1,289 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | The main loop of the game, processing player and AI moves turn by turn.-module Game.LambdaHack.Turn ( handleTurn ) where--import Control.Monad-import Control.Monad.State hiding (State, state)-import Control.Arrow ((&&&))-import qualified Data.List as L-import qualified Data.Ord as Ord-import qualified Data.Map as M-import qualified Data.IntMap as IM-import Data.Maybe--import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Action-import Game.LambdaHack.Actions-import Game.LambdaHack.EffectAction-import qualified Game.LambdaHack.Binding as Binding-import Game.LambdaHack.Actor-import Game.LambdaHack.ActorState-import Game.LambdaHack.Level-import Game.LambdaHack.State-import Game.LambdaHack.Strategy-import Game.LambdaHack.StrategyAction-import Game.LambdaHack.Running-import qualified Game.LambdaHack.Key as K-import Game.LambdaHack.Msg-import Game.LambdaHack.Draw-import qualified Game.LambdaHack.Kind as Kind-import Game.LambdaHack.Time-import Game.LambdaHack.Content.FactionKind-import Game.LambdaHack.Content.StrategyKind-import Game.LambdaHack.Random----- One clip proceeds through the following functions:------ handleTurn--- handleActors--- handleAI or handlePlayer--- handleActors--- handleAI or handlePlayer--- ...--- handleTurn (again)---- What's happening where:------ handleTurn: HP regeneration, monster generation, determine who moves next,--- dispatch to handlePlayer and handleActors, advance global game time------ handleActors: find an actor that can move, advance actor time,--- update perception, remember, push frame, repeat------ handlePlayer: update perception, remember, display frames,--- get and process commmands (zero or more), update smell map------ handleAI: determine and process actor's action---- | Start a clip (a part of a turn for which one or more frames--- will be generated). Do whatever has to be done--- every fixed number of time units, e.g., monster generation.--- Run the player and other actors moves. Eventually advance the time--- and repeat.-handleTurn :: Action ()-handleTurn = do- debug "handleTurn"- time <- gets stime -- the end time of this clip, inclusive- let clipN = (time `timeFit` timeClip) `mod` (timeTurn `timeFit` timeClip)- -- Regenerate HP and add monsters each turn, not each clip.- when (clipN == 1) regenerateLevelHP- when (clipN == 3) generateMonster- ptime <- gets (btime . getPlayerBody) -- time of player's next move- debug $ "handleTurn: time check. ptime ="- <+> showT ptime <> ", time =" <+> showT time- handleActors timeZero- modify (updateTime (timeAdd timeClip))- endOrLoop handleTurn---- TODO: We should replace this structure using a priority search queue/tree.--- | Perform moves for individual actors not controlled--- by the player, as long as there are actors--- with the next move time less than or equal to the current time.--- Some very fast actors may move many times a clip and then--- we introduce subclips and produce many frames per clip to avoid--- jerky movement. Otherwise we push exactly one frame or frame delay.-handleActors :: Time -- ^ the start time of current subclip, exclusive- -> Action ()-handleActors subclipStart = do- debug "handleActors"- Kind.COps{coactor} <- getCOps- sfaction <- gets sfaction- time <- gets stime -- the end time of this clip, inclusive- pl <- gets splayer- pbody <- gets getPlayerBody- -- Older actors act earlier, the player acts first.- lactor <- gets (((pl, pbody) :) . IM.toList . IM.delete pl . lactor . slevel)- squitOld <- gets squit- let mnext = if null lactor -- wait until any actor spawned- then Nothing- else let -- Heroes move first then monsters, then the rest.- order = Ord.comparing (btime . snd &&& bfaction . snd)- (actor, m) = L.minimumBy order lactor- in if btime m > time- then Nothing -- no actor is ready for another move- else Just (actor, m)- case mnext of- _ | isJust squitOld -> return ()- Nothing -> when (subclipStart == timeZero) $ displayFramePush Nothing- Just (actor, _) -> do- m <- gets (getActor actor)- if actor == pl- then- -- Player moves always start a new subclip.- startClip $ do- handlePlayer- squitNew <- gets squit- plNew <- gets splayer- -- Advance time once, after the player switched perhaps many times.- -- Ending and especially saving does not take time.- -- TODO: this is correct only when all heroes have the same- -- speed and can't switch players by, e.g., aiming a wand- -- of domination. We need to generalize by displaying- -- "(next move in .3s [RET]" when switching players.- -- RET waits .3s and gives back control,- -- Any other key does the .3s wait and the action form the key- -- at once. This requires quite a bit of refactoring- -- and is perhaps better done when the other factions have- -- selected players as well.- unless (isJust squitNew) $ advanceTime plNew- handleActors $ btime m- else do- advanceTime actor -- advance time while the actor still alive- let subclipStartDelta = timeAddFromSpeed coactor m subclipStart- if subclipStart == timeZero- || btime m > subclipStartDelta- || bfaction m == sfaction && not (bproj m)- then- -- That's the first move this clip- -- or the actor has already moved during this subclip- -- or it's a hero. In either case, start a new subclip.- startClip $ do- handleAI actor- handleActors $ btime m- else do- -- The monster didn't yet move this subclip.- handleAI actor- handleActors subclipStart---- | Handle the move of a single monster.-handleAI :: ActorId -> Action ()-handleAI actor = do- cops@Kind.COps{ cofact=Kind.Ops{okind}- , costrat=Kind.Ops{opick, okind=sokind}- } <- getCOps- state <- get- per <- getPerception- let Actor{bfaction, bloc, bsymbol} = getActor actor state- faction = okind bfaction- factionAi <- rndToAction $ opick (fAiIdle faction) (const True)- let factionAbilities = sabilities (sokind factionAi)- stratTarget = targetStrategy cops actor state per factionAbilities- -- Choose a target from those proposed by AI for the actor.- btarget <- rndToAction $ frequency $ bestVariant $ stratTarget- updateAnyActor actor $ \ m -> m { btarget }- stateNew <- get- let stratMove = strategy cops actor stateNew factionAbilities- debug $ "handleAI faction:" <+> fname faction- <> ", symbol:" <+> showT bsymbol- <> ", loc:" <+> showT bloc- <> "\nhandleAI target:" <+> showT stratTarget- <> "\nhandleAI move:" <+> showT stratMove- -- Run the AI: choses an action from those given by the AI strategy.- join $ rndToAction $ frequency $ bestVariant $ stratMove---- | Handle the move of the hero.-handlePlayer :: Action ()-handlePlayer = do- debug "handlePlayer"- -- When running, stop if aborted by a disturbance.- -- Otherwise let the player issue commands, until any of them takes time.- -- First time, just after pushing frames, ask for commands in Push mode.- tryWith (\ msg -> stopRunning >> playerCommand msg) $- ifRunning continueRun abort- addSmell---- | Determine and process the next player command. The argument is the last--- abort message due to running, if any.-playerCommand :: Msg -> Action ()-playerCommand msgRunAbort = do- -- The frame state is now Push.- Binding.Binding{kcmd} <- getBinding- kmPush <- case msgRunAbort of- "" -> getKeyCommand (Just True)- _ -> drawPrompt ColorFull msgRunAbort >>= getKeyFrameCommand- -- The frame state is now None and remains so between each pair- -- of lines of @loop@ (but can change within called actions).- let loop :: (K.Key, K.Modifier) -> Action ()- loop km = do- -- Messages shown, so update history and reset current report.- recordHistory- -- On abort, just reset state and call loop again below.- (timed, frames) <- tryWithFrame (return False) $ do- -- Look up the key.- case M.lookup km kcmd of- Just (_, declaredTimed, c) -> do- ((), frs) <- c- -- Targeting cursor movement and a few other subcommands- -- are wrongly marked as timed. This is indicated in their- -- definitions by setting @snoTime@ flag and used and reset here.- stakeTime <- gets stakeTime- let timed = fromMaybe declaredTimed stakeTime- modify (\ s -> s {stakeTime = Nothing})- -- Ensure at least one frame, if the command takes no time.- -- No frames for @abort@, so the code is here, not below.- if not timed && null (catMaybes frs)- then do- fr <- drawPrompt ColorFull ""- return (timed, [Just fr])- else return (timed, frs)- Nothing -> let msgKey = "unknown command <" <> K.showKM km <> ">"- in abortWith msgKey- -- The command was aborted or successful and if the latter,- -- possibly took some time.- if not timed- then do- -- If no time taken, rinse and repeat.- -- Analyse the obtained frames.- let (mfr, frs) = case reverse $ catMaybes frames of- [] -> (Nothing, [])- f : fs -> (Just f, reverse fs)- -- Show, one by one, all but the last frame.- -- Note: the code that generates the frames is responsible- -- for inserting the @more@ prompt.- b <- getOverConfirm frs- -- Display the last frame while waiting for the next key or,- -- if there is no next frame, just get the key.- kmNext <- case mfr of- Just fr | b -> getKeyFrameCommand fr- _ -> getKeyCommand Nothing- -- Look up and perform the next command.- loop kmNext- else do- -- Exit the loop and let other actors act. No next key needed- -- and no frames could have been generated.- assert (null frames `blame` length frames) $- return ()- loop kmPush---- | Advance (or rewind) the move time for the given actor.-advanceTime :: ActorId -> Action ()-advanceTime actor = do- Kind.COps{coactor} <- getCOps- let upd m@Actor{btime} = m {btime = timeAddFromSpeed coactor m btime}- updateAnyActor actor upd----- The issues below are now complicated (?) by the fact that we now generate--- a game screen frame at least once every clip and a jointed pair--- of frame+key input for each command that does not take time.------ Design thoughts (in order to get rid or partially rid of the somewhat--- convoluted design we have): We have three kinds of commands.------ Normal commands: they take time, so after handling the command, state changes,--- time passes and monsters get to move.------ Instant commands: they take no time, and do not change the state.------ Meta commands: they take no time, but may change the state.------ Ideally, they can all be handled via the same (event) interface. We maintain an--- event queue where we store what has to be handled next. The event queue is a sorted--- list where every event contains the timestamp when the event occurs. The current game--- time is equal to the head element of the event queue. Currently, we only have action--- events. An actor gets to move on an event. The actor is responsible for reinsterting--- itself in the event queue. Possible new events may include HP regeneration events,--- monster generation events, or actor death events.------ If an action does not take any time, the actor just reinserts itself with the current--- time into the event queue. If the insert algorithm makes sure that later events with--- the same time get precedence, this will work just fine.------ It's important that we decouple issues like HP regeneration from action events if we--- do it like that, because otherwise, HP regeneration may occur multiple times.------ Given this scheme, we may get orphaned events: a HP regeneration event for a dead--- monster may be scheduled. Or a move event for a monster suddenly put to sleep. We--- therefore have to given handlers the option of accessing and cleaning up the event--- queue.
Game/LambdaHack/Utils/Assert.hs view
@@ -1,8 +1,8 @@ -- | Tools for specifying assertions. A step towards contracts. -- Actually, a bunch of hacks wrapping the original @assert@ function,--- which is the only easy way of obtaining source locations.+-- which is the only easy way of obtaining source positions. module Game.LambdaHack.Utils.Assert- ( assert, blame, failure, allB, checkM, trueM, falseM+ ( assert, blame, failure, allB, skip ) where import Control.Exception (assert)@@ -23,7 +23,7 @@ in trace s False infix 1 `failure`--- | Like 'error', but shows the source location and also+-- | Like 'error', but shows the source position and also -- the value to blame for the failure. To be used as in: -- -- > assert `failure` ((x1, y1), (x2, y2), "designate a vertical line")@@ -34,7 +34,7 @@ " " ++ show blamed in trace s $ asrt False- (error "Assert.failure: no error location (upgrade to GHC >= 7.4)")+ (error "Assert.failure: no error position (upgrade to GHC >= 7.4)") -- | Like 'List.all', but if the predicate fails, blame all the list elements -- and especially those for which it fails. To be used as in:@@ -46,23 +46,9 @@ let s = show (filter (not . predicate) l) ++ " in the context of " ++ show l in blame (all predicate l) s --- | Check that the value returned from a monad action satisfies a predicate.--- Reports source location and the suspects. Drops the value.-checkM :: (Show a, Monad m) =>- (Bool -> m () -> m ()) -> (c -> Bool) -> a -> c -> m ()-checkM asrt predicate blamed value- | predicate value = return ()- | otherwise =- let s = "The returned value is wrong and the following is to blame:\n" ++- " " ++ show blamed- in trace s $- asrt False- (error "Assert.checkM: no error location (upgrade to GHC >= 7.4)")---- | Verifies that the returned value is true (respectively, false). Used as in:+-- | To be used in place of the verbose @skip@, as in: ----- > open newValve >>= assert `trueM` (newValve, "is already opened, not new")-trueM, falseM :: (Show a, Monad m) => (Bool -> m () -> m ()) -> a -> Bool- -> m ()-trueM asrt = checkM asrt id-falseM asrt = checkM asrt not+-- > do b <- getB a+-- > assert (b `blame` a) skip+skip :: Monad m => m ()+skip = return ()
Game/LambdaHack/Utils/File.hs view
@@ -1,12 +1,15 @@ -- | Saving/loading with serialization and compression. module Game.LambdaHack.Utils.File- ( encodeEOF, strictDecodeEOF+ ( encodeEOF, strictDecodeEOF, tryCreateDir, tryCopyDataFiles ) where -import System.IO+import qualified Codec.Compression.Zlib as Z+import Control.Monad import Data.Binary import qualified Data.ByteString.Lazy as LBS-import qualified Codec.Compression.Zlib as Z+import System.Directory+import System.FilePath+import System.IO -- | Serialize, compress and save data. -- Note that LBS.writeFile opens the file in binary mode.@@ -40,3 +43,21 @@ if n == "OK" then return a else error $ "Fatal error: corrupted file " ++ f++-- | Try to create a directory, if it doesn't exist. Terminate the program+-- with an exception if the directory does not exist, but can't be created.+tryCreateDir :: FilePath -> IO ()+tryCreateDir dir = do+ b <- doesDirectoryExist dir+ unless b $ createDirectory dir++-- | Try to copy over data files, if not already there.+tryCopyDataFiles :: (FilePath -> IO FilePath) -> [(FilePath, FilePath)]+ -> IO ()+tryCopyDataFiles pathsDataFile files =+ let cpFile (fin, fout) = do+ pathsDataIn <- pathsDataFile $ takeFileName fin+ bIn <- doesFileExist pathsDataIn+ bOut <- doesFileExist fout+ when (bIn && not bOut) $ copyFile pathsDataIn fout+ in mapM_ cpFile files
Game/LambdaHack/Utils/Frequency.hs view
@@ -11,12 +11,13 @@ , rollFreq, nullFreq, runFrequency, nameFrequency ) where +import Control.Arrow (first, second) import Control.Monad-import qualified System.Random as R import Data.Text (Text)+import qualified System.Random as R +import Game.LambdaHack.Common.Msg import Game.LambdaHack.Utils.Assert-import Game.LambdaHack.Msg -- TODO: do not expose runFrequency -- | The frequency distribution type.@@ -44,7 +45,7 @@ mzero = Frequency "[]" [] instance Functor Frequency where- fmap f (Frequency name xs) = Frequency name (map (\ (p, x) -> (p, f x)) xs)+ fmap f (Frequency name xs) = Frequency name (map (second f) xs) -- | Uniform discrete frequency distribution. uniformFreq :: Text -> [a] -> Frequency a@@ -60,7 +61,7 @@ scaleFreq :: Show a => Int -> Frequency a -> Frequency a scaleFreq n (Frequency name xs) = assert (n > 0 `blame` ("non-positive scale for" <+> name, n, xs)) $- Frequency name (map (\ (p, x) -> (n * p, x)) xs)+ Frequency name (map (first (* n)) xs) -- | Change the description of the frequency. renameFreq :: Text -> Frequency a -> Frequency a@@ -74,8 +75,8 @@ assert `failure` ("singleton frequency with nothing to pick:" <+> name, n, x) rollFreq (Frequency _ [(_, x)]) g = (x, g) -- speedup rollFreq (Frequency name fs) g =- assert (sumf > 0 `blame` ("frequency with nothing to pick:" <+> name, fs)) $- (frec r fs, ng)+ assert (sumf > 0 `blame` ("frequency with nothing to pick:" <+> name, fs))+ (frec r fs, ng) where sumf = sum (map fst fs) (r, ng) = R.randomR (1, sumf) g
Game/LambdaHack/Utils/LQueue.hs view
@@ -1,6 +1,8 @@ -- | Queues implemented with two stacks to ensure fast writes. module Game.LambdaHack.Utils.LQueue- ( LQueue, newLQueue, nullLQueue, trimLQueue, tryReadLQueue, writeLQueue+ ( LQueue+ , newLQueue, nullLQueue, lengthLQueue, tryReadLQueue, writeLQueue+ , trimLQueue, dropStartLQueue, lastLQueue, toListLQueue ) where import Data.Maybe@@ -16,13 +18,9 @@ nullLQueue :: LQueue a -> Bool nullLQueue (rs, ws) = null rs && null ws --- | Remove all but the last written non-@Nothing@ element of the queue.-trimLQueue :: LQueue (Maybe a) -> LQueue (Maybe a)-trimLQueue (rs, ws) =- let trim (_, w:_) = ([w], [])- trim ([], []) = ([], [])- trim (rsj, []) = ([last rsj], [])- in trim (filter isJust rs, filter isJust ws)+-- | The length of the queue.+lengthLQueue :: LQueue a -> Int+lengthLQueue (rs, ws) = length rs + length ws -- | Try reading a queue. Return @Nothing@ if empty. tryReadLQueue :: LQueue a -> Maybe (a, LQueue a)@@ -33,3 +31,29 @@ -- | Write to the queue. Faster than reading. writeLQueue :: LQueue a -> a -> LQueue a writeLQueue (rs, ws) w = (rs, w : ws)++-- | Remove all but the last written non-@Nothing@ element of the queue.+trimLQueue :: LQueue (Maybe a) -> LQueue (Maybe a)+trimLQueue (rs, ws) =+ let trim (_, w:_) = ([w], [])+ trim ([], []) = ([], [])+ trim (rsj, []) = ([last rsj], [])+ in trim (filter isJust rs, filter isJust ws)++-- | Remove frames up to and including the first segment of @Nothing@ frames.+-- | If the resulting queue is empty, apply trimLQueue instead.+dropStartLQueue :: LQueue (Maybe a) -> LQueue (Maybe a)+dropStartLQueue (rs, ws) =+ let dq = (dropWhile isNothing $ dropWhile isJust $ rs ++ reverse ws, [])+ in if nullLQueue dq then trimLQueue (rs, ws) else dq++-- | Dump all but the last written non-@Nothing@ element of the queue, if any.+lastLQueue :: LQueue (Maybe a) -> Maybe a+lastLQueue (rs, ws) =+ let lst (_, w:_) = Just w+ lst ([], []) = Nothing+ lst (rsj, []) = Just $ last rsj+ in lst (catMaybes rs, catMaybes ws)++toListLQueue :: LQueue a -> [a]+toListLQueue (rs, ws) = rs ++ reverse ws
− Game/LambdaHack/Vector.hs
@@ -1,163 +0,0 @@--- | Basic operations on 2D vectors represented in an efficient,--- but not unique, way.-module Game.LambdaHack.Vector- ( Vector, toVector, shift, shiftBounded, moves, movesWidth- , isUnit, euclidDistSq, diagonal, neg, towards, displacement- , displacePath, shiftPath- ) where--import Data.Binary--import Game.LambdaHack.PointXY-import Game.LambdaHack.VectorXY-import Game.LambdaHack.Area-import Game.LambdaHack.Point-import Game.LambdaHack.Utils.Assert---- | 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 (Show, Eq)--instance Binary Vector where- put (Vector dir) = put dir- get = fmap Vector get---- | Converts a vector in cartesian representation into @Vector@.-toVector :: X -> VectorXY -> Vector-toVector lxsize (VectorXY (x, y)) =- Vector $ x + y * lxsize--isUnitXY :: VectorXY -> Bool-isUnitXY v = chessDistXY v == 1---- | Tells if a vector has length 1 in the chessboard metric.-isUnit :: X -> Vector -> Bool-isUnit lxsize = isUnitXY . fromDir lxsize---- | 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` (lxsize, v)) $- Vector $ x + y * lxsize---- | 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` (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---- | Translate a point by a vector.------ Particularly simple and fast implementation in the linear representation.-shift :: Point -> Vector -> Point-shift loc (Vector dir) = loc + dir---- | Translate a point by a vector, but only if the result fits in an area.-shiftBounded :: X -> Area -> Point -> Vector -> Point-shiftBounded lxsize area loc dir =- let res = shift loc dir- in if inside lxsize res area then res else loc---- | Vectors of all unit moves, clockwise, starting north-west.-moves :: X -> [Vector]-moves lxsize = map (toDir lxsize) movesXY---- | Vectors of all unit moves, clockwise, starting north-west,--- parameterized by level width.-movesWidth :: [X -> Vector]-movesWidth = map (flip toDir) movesXY---- | 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)---- | Checks whether a unit vector is a diagonal direction,--- as opposed to cardinal.-diagonal :: X -> Vector -> Bool-diagonal lxsize dir | VectorXY (x, y) <- fromDir lxsize dir =- x * y /= 0---- | Reverse an arbirary vector.-neg :: Vector -> Vector-neg (Vector dir) = Vector (-dir)---- 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)) =- assert (dx /= 0 || dy /= 0 `blame` (dx, dy)) $- let angle :: Double- angle = atan (fromIntegral dy / fromIntegral dx) / (pi / 2)- dxy | angle <= -0.75 = (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` (lxsize, dx, dy, angle)- rxy = if dx >= 0- then VectorXY dxy- else negXY $ VectorXY dxy- in assert ((if isUnitXY v then v == rxy else True)- `blame` (v, rxy))- $ toDir lxsize rxy---- TODO: Perhaps produce all acceptable directions and let AI choose.--- That would also eliminate the Doubles. Or only directions from bla?--- Smart monster could really use all dirs to be less predictable,--- but it wouldn't look as natural as bla, so for less smart bla is better.--- | Given two distinct locations, determine the direction (a unit vector)--- in which one should move from the first in order to get closer--- to the second. Ignores obstacles. Of several equally good directions--- (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 loc0 loc1 =- assert (loc0 /= loc1 `blame` (loc0, loc1)) $- let v = displacementXYZ lxsize loc0 loc1- in normalize lxsize v---- | A vector from a point to another. We have------ > shift loc1 (displacement loc1 loc2) == loc2------ Particularly simple and fast implementation in the linear representation.-displacement :: Point -> Point -> Vector-displacement loc1 loc2 = Vector $ loc2 - loc1---- | A list of vectors between a list of points.-displacePath :: [Point] -> [Vector]-displacePath [] = []-displacePath lp1@(_ : lp2) =- map (uncurry displacement) $ zip lp1 lp2---- | 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
− Game/LambdaHack/VectorXY.hs
@@ -1,39 +0,0 @@--- | Basic cartesian geometry operations on 2D vectors.-module Game.LambdaHack.VectorXY- ( VectorXY(..), shiftXY, movesXY, movesCardinalXY- , chessDistXY, euclidDistSqXY, negXY- ) where--import Game.LambdaHack.PointXY---- | 2D vectors in cartesian representation.-newtype VectorXY = VectorXY (X, Y)- deriving (Show, Eq)---- | 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)
LambdaHack.cabal view
@@ -1,5 +1,5 @@ name: LambdaHack-version: 0.2.6.5+version: 0.2.8 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@@ -13,46 +13,45 @@ Several frontends are available (GTK is the default) and many other generic engine components are easily overridden, but the fundamental source of flexibility lies- in the strict and type-safe separation of code and content.- .- This is a minor release, primarily intended to fix broken- haddock documentation on Hackage by disabling gtk2hs dependency- under GHC 7.6.1 (if you use GHC 7.6.1 and gtk2hs compiles- for you, please run 'cabal install -fgtk --reinstall').- Changes since 0.2.6 are mostly unrelated to gameplay:- strictly typed config files split into UI and rules;- a switch from Text to String throughout the codebase;- use of the external library 'miniutter' for English sentence- generation.+ in the strict and type-safe separation of code and content+ and of clients (human and AI-controlled) and server. .- Upcoming new features: playable monsters faction, more than- two factions inhabiting the dungeon, AIvAI, PvP, improved- ranged combat AI, dynamic light sources, explosions- player action undo/redo, completely redesigned UI. Long term- goals are focused around procedural content generation- and include in-game content creation, auto-balancing- and persistent content modification based on player behaviour.+ New, in this release, is cooperative and competitive multiplayer+ (shared-screen only in this version) and overhauled searching.+ The code has been rewritten 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.+ Upcoming features: new and improved frontends, improved AI+ (better leader switching and 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. . A larger game that depends on the LambdaHack library is Allure of the Stars, available from <http://hackage.haskell.org/package/Allure>. .- Note: All modules in the library are kept visible,- to let games override each, but reuse as many as possible.+ Note: All modules in this library are kept visible,+ to let games override and reuse them. OTOH, to reflect that some modules are implementation details relative to others, the source code adheres to the following convention. If a module has the same name as a directory, the module is the exclusive interface to the directory. No references to the modules in the directory are allowed- except from the interface module.+ except from the interface module. This policy is only binding+ inside the library --- users are free to do whatever they+ please, since the library authors are in no position to guess+ their particular needs. homepage: http://github.com/kosmikus/LambdaHack bug-reports: http://github.com/kosmikus/LambdaHack/issues license: BSD3 license-file: LICENSE-tested-with: GHC == 7.2.2, GHC == 7.4.1, GHC == 7.6.1+tested-with: GHC == 7.4.2, GHC == 7.6.1 data-files: LICENSE, CREDITS, PLAYING.md, README.md,- config.rules.default, config.ui.default, config.rules.bot,- scores+ config.rules.default, config.ui.default, scores author: Andres Loeh, Mikolaj Konarski maintainer: Mikolaj Konarski <mikolaj.konarski@funktory.com> category: Game Engine@@ -80,28 +79,59 @@ default: False library- exposed-modules: Game.LambdaHack.Ability,- Game.LambdaHack.Action,- Game.LambdaHack.Action.ActionLift- Game.LambdaHack.Action.ConfigIO- Game.LambdaHack.Action.Frontend,- Game.LambdaHack.Action.Frontend.Chosen,- Game.LambdaHack.Action.HighScore,- Game.LambdaHack.Action.Save,- Game.LambdaHack.Actions,- Game.LambdaHack.Actor,- Game.LambdaHack.ActorState,- Game.LambdaHack.Animation,- Game.LambdaHack.Area,- Game.LambdaHack.AreaRnd,- Game.LambdaHack.Binding,- Game.LambdaHack.BindingAction,- Game.LambdaHack.CDefs,- Game.LambdaHack.Cave,- Game.LambdaHack.Color,- Game.LambdaHack.Command,- Game.LambdaHack.CommandAction,- Game.LambdaHack.Config,+ exposed-modules: Game.LambdaHack.Client,+ Game.LambdaHack.Client.Action,+ Game.LambdaHack.Client.Action.ActionClass,+ Game.LambdaHack.Client.Action.ActionType,+ Game.LambdaHack.Client.Action.ConfigIO,+ Game.LambdaHack.Client.Action.Save,+ Game.LambdaHack.Client.AtomicSemCli,+ Game.LambdaHack.Client.Binding,+ 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,+ Game.LambdaHack.Client.LoopAction,+ Game.LambdaHack.Client.RunAction,+ Game.LambdaHack.Client.State,+ Game.LambdaHack.Client.Strategy,+ Game.LambdaHack.Client.StrategyAction,+ Game.LambdaHack.Common.Ability,+ Game.LambdaHack.Common.Action,+ Game.LambdaHack.Common.Actor,+ Game.LambdaHack.Common.ActorState,+ Game.LambdaHack.Common.Animation,+ Game.LambdaHack.Common.Area,+ Game.LambdaHack.Common.AtomicCmd,+ Game.LambdaHack.Common.AtomicPos,+ Game.LambdaHack.Common.AtomicSem,+ Game.LambdaHack.Common.ClientCmd,+ Game.LambdaHack.Common.Color,+ 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.Item,+ 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.Point,+ Game.LambdaHack.Common.PointXY,+ Game.LambdaHack.Common.Random,+ Game.LambdaHack.Common.ServerCmd,+ Game.LambdaHack.Common.State,+ 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,@@ -110,87 +140,87 @@ Game.LambdaHack.Content.RuleKind, Game.LambdaHack.Content.StrategyKind, Game.LambdaHack.Content.TileKind,- Game.LambdaHack.Draw,- Game.LambdaHack.Dungeon,- Game.LambdaHack.DungeonState,- Game.LambdaHack.Effect,- Game.LambdaHack.EffectAction,- Game.LambdaHack.Feature,- Game.LambdaHack.Flavour,- Game.LambdaHack.FOV,- Game.LambdaHack.FOV.Common,- Game.LambdaHack.FOV.Digital,- Game.LambdaHack.FOV.Permissive,- Game.LambdaHack.FOV.Shadow,- Game.LambdaHack.Item,- Game.LambdaHack.ItemAction,- Game.LambdaHack.Key,- Game.LambdaHack.Kind,- Game.LambdaHack.Level,- Game.LambdaHack.Misc,- Game.LambdaHack.Msg,- Game.LambdaHack.Perception,- Game.LambdaHack.Place,- Game.LambdaHack.Point,- Game.LambdaHack.PointXY,- Game.LambdaHack.Random,- Game.LambdaHack.Running,- Game.LambdaHack.State,- Game.LambdaHack.Strategy,- Game.LambdaHack.StrategyAction,- Game.LambdaHack.Tile,- Game.LambdaHack.Time,- Game.LambdaHack.Turn,+ Game.LambdaHack.Frontend,+ Game.LambdaHack.Frontend.Chosen,+ Game.LambdaHack.Server,+ Game.LambdaHack.Server.Action,+ Game.LambdaHack.Server.Action.ActionClass,+ Game.LambdaHack.Server.Action.ActionType,+ Game.LambdaHack.Server.Action.ConfigIO,+ Game.LambdaHack.Server.Action.Save,+ Game.LambdaHack.Server.AtomicSemSer,+ Game.LambdaHack.Server.Config,+ Game.LambdaHack.Server.DungeonGen,+ Game.LambdaHack.Server.DungeonGen.AreaRnd,+ Game.LambdaHack.Server.DungeonGen.Cave,+ Game.LambdaHack.Server.DungeonGen.Place,+ Game.LambdaHack.Server.EffectSem,+ Game.LambdaHack.Server.Fov,+ Game.LambdaHack.Server.Fov.Common,+ Game.LambdaHack.Server.Fov.Digital,+ Game.LambdaHack.Server.Fov.Permissive,+ Game.LambdaHack.Server.Fov.Shadow,+ Game.LambdaHack.Server.LoopAction,+ Game.LambdaHack.Server.ServerSem,+ Game.LambdaHack.Server.StartAction,+ Game.LambdaHack.Server.State, Game.LambdaHack.Utils.Assert, Game.LambdaHack.Utils.File, Game.LambdaHack.Utils.Frequency,- Game.LambdaHack.Utils.LQueue,- Game.LambdaHack.Vector- Game.LambdaHack.VectorXY+ Game.LambdaHack.Utils.LQueue other-modules: Paths_LambdaHack build-depends: ConfigFile >= 1.1.1 && < 2, array >= 0.3.0.3 && < 1, base >= 4 && < 5,- binary >= 0.5.0.2 && < 1,+ binary >= 0.7 && < 1, bytestring >= 0.9.2 && < 1,- containers >= 0.4.1 && < 1,+ containers >= 0.5 && < 1,+ deepseq >= 1.3 && < 2, directory >= 1.1.0.1 && < 2,+ enummapset >= 0.5.2 && < 1, filepath >= 1.2.0.1 && < 2,- miniutter >= 0.4.0 && < 2,+ ghc-prim >= 0.2,+ hashable >= 1.2 && < 2,+ keys >= 3 && < 4,+ miniutter >= 0.4.1 && < 2, mtl >= 2.0.1 && < 3, old-time >= 1.0.0.7 && < 2, random >= 1.0.1 && < 2,+ stm >= 2.4 && < 3, text >= 0.11.2.3 && < 1,+ transformers >= 0.3 && < 1,+ unordered-containers >= 0.2.3 && < 1, zlib >= 0.5.3.1 && < 1 default-language: Haskell2010- default-extensions: MonoLocalBinds,+ default-extensions: MonoLocalBinds, ScopedTypeVariables, BangPatterns, RecordWildCards, NamedFieldPuns- other-extensions: CPP, MultiParamTypeClasses, RankNTypes,- ScopedTypeVariables, TypeFamilies,- OverloadedStrings, ExtendedDefaultRules+ other-extensions: CPP, MultiParamTypeClasses, RankNTypes, TypeFamilies,+ OverloadedStrings, FlexibleContexts, FlexibleInstances,+ FunctionalDependencies, GeneralizedNewtypeDeriving,+ DeriveFunctor, TupleSections+--, DeriveGeneric ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude ghc-options: -fno-ignore-asserts -funbox-strict-fields if flag(gtk) {- other-modules: Game.LambdaHack.Action.Frontend.Gtk+ other-modules: Game.LambdaHack.Frontend.Gtk build-depends: gtk >= 0.12.1 && < 0.13 cpp-options: -DGTK } else { if flag(vty) {- other-modules: Game.LambdaHack.Action.Frontend.Vty+ other-modules: Game.LambdaHack.Frontend.Vty build-depends: vty >= 4.7.0.6 cpp-options: -DVTY } else { if flag(curses) {- other-modules: Game.LambdaHack.Action.Frontend.Curses+ other-modules: Game.LambdaHack.Frontend.Curses build-depends: hscurses >= 1.4.1 && < 2 cpp-options: -DCURSES--- a hack to work around gtk2hs problems on Hackage under GHC 7.6.1:- } else { if flag(std) || impl(ghc == 7.6.1) {- other-modules: Game.LambdaHack.Action.Frontend.Std+ } else { if flag(std) {+ other-modules: Game.LambdaHack.Frontend.Std cpp-options: -DSTD } else {- other-modules: Game.LambdaHack.Action.Frontend.Gtk+ other-modules: Game.LambdaHack.Frontend.Gtk build-depends: gtk >= 0.12.1 && < 0.13 } } } } @@ -213,22 +243,30 @@ ConfigFile >= 1.1.1 && < 2, array >= 0.3.0.3 && < 1, base >= 4 && < 5,- binary >= 0.5.0.2 && < 1,+ binary >= 0.7 && < 1, bytestring >= 0.9.2 && < 1,- containers >= 0.4.1 && < 1,+ containers >= 0.5 && < 1,+ deepseq >= 1.3 && < 2, directory >= 1.1.0.1 && < 2,+ enummapset >= 0.5.2 && < 1, filepath >= 1.2.0.1 && < 2,- miniutter >= 0.4.0 && < 2,+ ghc-prim >= 0.2,+ hashable >= 1.2 && < 2,+ keys >= 3 && < 4,+ miniutter >= 0.4.1 && < 2, mtl >= 2.0.1 && < 3, old-time >= 1.0.0.7 && < 2, random >= 1.0.1 && < 2,+ stm >= 2.4 && < 3, text >= 0.11.2.3 && < 1,+ transformers >= 0.3 && < 1,+ unordered-containers >= 0.2.3 && < 1, zlib >= 0.5.3.1 && < 1 default-language: Haskell2010- default-extensions: MonoLocalBinds,+ default-extensions: MonoLocalBinds, ScopedTypeVariables, BangPatterns, RecordWildCards, NamedFieldPuns- other-extensions: CPP, QuasiQuotes, OverloadedStrings, ExtendedDefaultRules+ other-extensions: CPP, QuasiQuotes, OverloadedStrings ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude ghc-options: -fno-ignore-asserts -funbox-strict-fields@@ -242,7 +280,7 @@ random >= 1.0.1 && < 2 default-language: Haskell2010- default-extensions: MonoLocalBinds,+ default-extensions: MonoLocalBinds, ScopedTypeVariables, BangPatterns, RecordWildCards, NamedFieldPuns ghc-options: -Wall -fwarn-orphans -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unrecognised-pragmas ghc-options: -fno-warn-auto-orphans -fno-warn-implicit-prelude
LambdaHack/Content/ActorKind.hs view
@@ -2,15 +2,15 @@ -- | Monsters and heroes for LambdaHack. module Content.ActorKind ( cdefs ) where -import Game.LambdaHack.Ability-import Game.LambdaHack.Color-import Game.LambdaHack.CDefs+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-import Game.LambdaHack.Random-import Game.LambdaHack.Time -cdefs :: CDefs ActorKind-cdefs = CDefs+cdefs :: ContentDef ActorKind+cdefs = ContentDef { getSymbol = asymbol , getName = aname , getFreq = afreq@@ -23,13 +23,13 @@ hero = ActorKind { asymbol = '@' , aname = "hero"- , afreq = [("hero", 1)] -- Does not appear randomly in the dungeon.- , acolor = BrWhite -- Heroes white, monsters colorful.- , ahp = RollDice 60 1+ , afreq = [("hero", 1)]+ , acolor = BrWhite -- modified if many hero factions+ , ahp = RollDice 50 1 , aspeed = toSpeed 2 , asight = True , asmell = False- , aiq = 13 -- Can see hidden doors, when he is under alien control.+ , aiq = 16 , aregen = 500 , acanDo = [minBound..maxBound] }@@ -51,7 +51,7 @@ eye = ActorKind { asymbol = 'e' , aname = "reducible eye"- , afreq = [("monster", 60), ("summon", 50)]+ , afreq = [("monster", 60), ("horror", 60)] , acolor = BrRed , ahp = RollDice 7 4 , aspeed = toSpeed 2@@ -64,7 +64,7 @@ fastEye = ActorKind { asymbol = 'e' , aname = "super-fast eye"- , afreq = [("monster", 15)]+ , afreq = [("monster", 15), ("horror", 15)] , acolor = BrBlue , ahp = RollDice 1 4 , aspeed = toSpeed 4@@ -77,7 +77,7 @@ nose = ActorKind { asymbol = 'n' , aname = "point-free nose"- , afreq = [("monster", 20), ("summon", 100)]+ , afreq = [("monster", 20), ("horror", 20)] , acolor = Green , ahp = RollDice 17 2 , aspeed = toSpeed 1.8
LambdaHack/Content/CaveKind.hs view
@@ -4,21 +4,21 @@ import Data.Ratio -import Game.LambdaHack.CDefs-import Game.LambdaHack.Random as Random+import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Random as Random import Game.LambdaHack.Content.CaveKind-import Game.LambdaHack.Misc -cdefs :: CDefs CaveKind-cdefs = CDefs+cdefs :: ContentDef CaveKind+cdefs = ContentDef { getSymbol = csymbol , getName = cname , getFreq = cfreq , validate = cvalidate , content =- [rogue, arena, empty, noise]+ [rogue, arena, empty, noise, combat] }-rogue, arena, empty, noise :: CaveKind+rogue, arena, empty, noise, combat :: CaveKind rogue = CaveKind { csymbol = '$'@@ -35,14 +35,13 @@ , cminStairDist = 30 , cdoorChance = 1%2 , copenChance = 1%10- , chiddenChance = 1%5+ , chidden = 8 , citemNum = RollDice 7 2- , cdefaultTile = "fillerWall"+ , cdefTile = "fillerWall" , ccorridorTile = "darkCorridor" , cfillerTile = "fillerWall" , cdarkLegendTile = "darkLegend" , clitLegendTile = "litLegend"- , chiddenTile = "hidden" } arena = rogue { csymbol = 'A'@@ -53,8 +52,9 @@ , cdarkChance = (RollDice 1 80, RollDice 1 60) , cvoidChance = 1%3 , cnonVoidMin = 2+ , chidden = 9 , citemNum = RollDice 4 2 -- few rooms- , cdefaultTile = "floorArenaLit"+ , cdefTile = "floorArenaLit" , ccorridorTile = "path" } empty = rogue@@ -68,8 +68,9 @@ , cvoidChance = 3%4 , cnonVoidMin = 1 , cminStairDist = 50+ , chidden = 10 , citemNum = RollDice 8 2 -- whole floor strewn with treasure- , cdefaultTile = "floorRoomLit"+ , cdefTile = "floorRoomLit" , ccorridorTile = "floorRoomLit" } noise = rogue@@ -81,7 +82,22 @@ , cdarkChance = (RollDice 1 80, RollDice 1 40) , cvoidChance = 0 , cnonVoidMin = 0+ , chidden = 6 , citemNum = RollDice 4 2 -- few rooms- , cdefaultTile = "noiseSet"+ , cdefTile = "noiseSet"+ , ccorridorTile = "path"+ }+combat = rogue+ { csymbol = 'C'+ , cname = "Combat arena"+ , cfreq = [("caveCombat", 1)]+ , cgrid = RollDiceXY (RollDice 5 2, RollDice 2 2)+ , cminPlaceSize = RollDiceXY (RollDice 1 1, RollDice 1 1)+ , cdarkChance = (RollDice 1 100, RollDice 1 100)+ , cvoidChance = 1%10+ , cnonVoidMin = 8+ , chidden = 100+ , citemNum = RollDice 12 2+ , cdefTile = "combatSet" , ccorridorTile = "path" }
LambdaHack/Content/FactionKind.hs view
@@ -1,37 +1,42 @@ {-# LANGUAGE OverloadedStrings #-}--- | Game factions (heroes, enemies, NPCs, etc.) for LambdaHack.+-- | The type of kinds of game factions (heroes, enemies, NPCs, etc.)+-- for LambdaHack. module Content.FactionKind ( cdefs ) where -import Game.LambdaHack.CDefs+import Game.LambdaHack.Common.ContentDef import Game.LambdaHack.Content.FactionKind -cdefs :: CDefs FactionKind-cdefs = CDefs+cdefs :: ContentDef FactionKind+cdefs = ContentDef { getSymbol = fsymbol , getName = fname , getFreq = ffreq , validate = fvalidate , content =- [hero, monster]+ [hero, monster, horror] }-hero, monster :: FactionKind+hero, monster, horror :: FactionKind hero = FactionKind- { fsymbol = '@'- , fname = "hero"- , ffreq = [("hero", 1), ("playable", 50)]- , fAiSelected = "noAbility" -- no AI, fully manual control- , fAiIdle = "meleeAdjacent"- , fenemy = ["monster"]- , fally = []+ { fsymbol = '@'+ , fname = "hero"+ , ffreq = [("hero", 1)]+ , fAiLeader = "fullAbility"+ , fAiMember = "meleeAdjacent" } monster = FactionKind- { fsymbol = 'm'- , fname = "monster"- , ffreq = [("monster", 1), ("playable", 50), ("spawn", 1)]- , fAiSelected = "fullAbility"- , fAiIdle = "fullAbility"- , fenemy = ["hero"]- , fally = []+ { fsymbol = 'm'+ , fname = "monster"+ , ffreq = [("monster", 1), ("spawn", 50), ("summon", 50)]+ , fAiLeader = "fullAbility"+ , fAiMember = "fullAbility"+ }++horror = FactionKind+ { fsymbol = 'h'+ , fname = "horror"+ , ffreq = [("horror", 1), ("summon", 50)]+ , fAiLeader = "fullAbility"+ , fAiMember = "fullAbility" }
LambdaHack/Content/ItemKind.hs view
@@ -2,25 +2,25 @@ -- | Weapons and treasure for LambdaHack. module Content.ItemKind ( cdefs ) where -import Game.LambdaHack.Color-import Game.LambdaHack.CDefs-import Game.LambdaHack.Effect-import Game.LambdaHack.Flavour-import Game.LambdaHack.Random+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 :: CDefs ItemKind-cdefs = CDefs+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, wand, fist, foot, tentacle, weight]+ [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, wand, fist, foot, tentacle, weight :: ItemKind+amulet, dart, gem1, gem2, gem3, currency, harpoon, potion1, potion2, potion3, ring, scroll1, scroll2, scroll3, sword, wand1, wand2, fist, foot, tentacle :: ItemKind -gem, potion, scroll :: ItemKind -- generic templates+gem, potion, scroll, wand :: ItemKind -- generic templates -- rollDeep (aDb, xDy) = rollDice aDb + lvl * rollDice xDy / depth @@ -29,9 +29,8 @@ , iname = "amulet" , ifreq = [("dng", 6)] , iflavour = zipFancy [BrGreen]- , ieffect = Regeneration+ , ieffect = Regeneration (RollDice 2 3, RollDice 1 10) , icount = intToDeep 1- , ipower = (RollDice 2 3, RollDice 1 10) , iverbApply = "tear down" , iverbProject = "cast" , iweight = 30@@ -42,9 +41,8 @@ , iname = "dart" , ifreq = [("dng", 30)] , iflavour = zipPlain [Cyan]- , ieffect = Wound (RollDice 1 1)+ , ieffect = Hurt (RollDice 1 1) (RollDice 1 2, RollDice 1 2) , icount = (RollDice 3 3, RollDice 0 0)- , ipower = intToDeep 0 , iverbApply = "snap" , iverbProject = "hurl" , iweight = 50@@ -57,7 +55,6 @@ , iflavour = zipPlain brightCol -- natural, so not fancy , ieffect = NoEffect , icount = intToDeep 0- , ipower = intToDeep 0 , iverbApply = "crush" , iverbProject = "toss" , iweight = 50@@ -75,11 +72,10 @@ currency = ItemKind { isymbol = '$' , iname = "gold piece"- , ifreq = [("dng", 80), ("currency", 1)]+ , ifreq = [("dng", 50), ("currency", 1)] , iflavour = zipPlain [BrYellow] , ieffect = NoEffect , icount = (RollDice 0 0, RollDice 10 10)- , ipower = intToDeep 0 , iverbApply = "grind" , iverbProject = "toss" , iweight = 31@@ -90,9 +86,8 @@ , iname = "harpoon" , ifreq = [("dng", 30)] , iflavour = zipPlain [Brown]- , ieffect = Wound (RollDice 1 2)+ , ieffect = Hurt (RollDice 1 2) (RollDice 1 2, RollDice 2 2) , icount = (RollDice 0 0, RollDice 2 2)- , ipower = (RollDice 1 1, RollDice 2 2) , iverbApply = "break up" , iverbProject = "hurl" , iweight = 4000@@ -105,7 +100,6 @@ , iflavour = zipFancy stdCol , ieffect = NoEffect , icount = intToDeep 1- , ipower = intToDeep 0 , iverbApply = "gulp down" , iverbProject = "lob" , iweight = 200@@ -116,22 +110,19 @@ , ieffect = ApplyPerfume } potion2 = potion- { ieffect = Heal- , ipower = (RollDice 5 1, RollDice 0 0)+ { ieffect = Heal 5 } potion3 = potion { ifreq = [("dng", 5)]- , ieffect = Wound (RollDice 0 0)- , ipower = (RollDice 5 1, RollDice 0 0)+ , ieffect = Heal (-5) } ring = ItemKind { isymbol = '=' , iname = "ring"- , ifreq = [("dng", 10)]+ , ifreq = [] -- [("dng", 10)] -- TODO: make it useful , iflavour = zipPlain [White]- , ieffect = Searching+ , ieffect = Searching (RollDice 1 6, RollDice 3 2) , icount = intToDeep 1- , ipower = (RollDice 1 6, RollDice 3 2) , iverbApply = "squeeze down" , iverbProject = "toss" , iweight = 15@@ -144,29 +135,28 @@ , iflavour = zipFancy darkCol -- arcane and old , ieffect = NoEffect , icount = intToDeep 1- , ipower = intToDeep 0 , iverbApply = "decipher" , iverbProject = "lob" , iweight = 50 , itoThrow = -75 -- bad shape, even rolled up } scroll1 = scroll- { ieffect = SummonFriend+ { ieffect = CallFriend 1+ , ifreq = [("dng", 2)] } scroll2 = scroll- { ieffect = SummonEnemy+ { ieffect = Summon 1 } scroll3 = scroll- { ieffect = Descend+ { ieffect = Descend 1 } sword = ItemKind { isymbol = ')' , iname = "sword"- , ifreq = [("dng", 60)]+ , ifreq = [("dng", 40)] , iflavour = zipPlain [BrCyan]- , ieffect = Wound (RollDice 3 1)+ , ieffect = Hurt (RollDice 3 1) (RollDice 1 2, RollDice 4 2) , icount = intToDeep 1- , ipower = (RollDice 1 2, RollDice 4 2) , iverbApply = "hit" , iverbProject = "heave" , iweight = 2000@@ -175,43 +165,43 @@ wand = ItemKind { isymbol = '/' , iname = "wand"- , ifreq = [("dng", 15)]- , iflavour = zipFancy [BrRed]- , ieffect = Dominate+ , ifreq = [("dng", 10)]+ , iflavour = zipFancy brightCol+ , ieffect = NoEffect , icount = intToDeep 1- , ipower = intToDeep 0 , iverbApply = "snap" , iverbProject = "zap" , iweight = 300 , itoThrow = 25 -- magic }+wand1 = wand+ { ieffect = Dominate+ }+wand2 = wand+ { ifreq = [("dng", 3)]+ , ieffect = Heal (-25)+ } fist = sword { isymbol = '@' , iname = "fist"- , ifreq = [("unarmed", 100)]+ , 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 = [("unarmed", 50)]+ , 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 = [("monstrous", 100)]+ , ifreq = [("hth", 1), ("monstrous", 100)]+ , ieffect = Hurt (RollDice 3 1) (intToDeep 0) , iverbApply = "hit" , iverbProject = "ERROR, please report: iverbProject tentacle"- }-weight = sword- { isymbol = '@'- , iname = "power jump"- , ifreq = [("weight", 100)]- , ieffect = Wound (RollDice 99 99)- , ipower = (RollDice 1 99, RollDice 0 0)- , iverbApply = "squash"- , iverbProject = "ERROR, please report: iverbProject weight" }
LambdaHack/Content/PlaceKind.hs view
@@ -2,11 +2,11 @@ -- | Rooms, halls and passages for LambdaHack. module Content.PlaceKind ( cdefs ) where -import Game.LambdaHack.CDefs import Game.LambdaHack.Content.PlaceKind+import Game.LambdaHack.Common.ContentDef -cdefs :: CDefs PlaceKind-cdefs = CDefs+cdefs :: ContentDef PlaceKind+cdefs = ContentDef { getSymbol = psymbol , getName = pname , getFreq = pfreq@@ -37,7 +37,7 @@ , "|.O.." , "|...." , "|...."- ]+ ] } pillarC = pillar { ptopLeft = [ "-----"@@ -45,7 +45,7 @@ , "|...." , "|...." , "|...."- ]+ ] } pillar3 = pillar { pfreq = [("rogue", 100)] -- Feels a bit intrusive with so many pillars.@@ -54,7 +54,7 @@ , "|...." , "|O..." , "|...."- ]+ ] } colonnade = PlaceKind { psymbol = 'c'
LambdaHack/Content/RuleKind.hs view
@@ -1,20 +1,19 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP, QuasiQuotes #-}+{-# LANGUAGE CPP, OverloadedStrings, QuasiQuotes #-} -- | Game rules and assorted game setup data for LambdaHack. module Content.RuleKind ( cdefs ) where -- Cabal import qualified Paths_LambdaHack as Self (getDataFileName, version) -import Game.LambdaHack.Vector+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-import qualified Game.LambdaHack.Feature as F-import Game.LambdaHack.CDefs import Multiline -cdefs :: CDefs RuleKind-cdefs = CDefs+cdefs :: ContentDef RuleKind+cdefs = ContentDef { getSymbol = rsymbol , getName = rname , getFreq = rfreq@@ -22,21 +21,21 @@ , content = [standard] }-standard :: RuleKind +standard :: RuleKind standard = RuleKind { rsymbol = 's' , rname = "standard LambdaHack ruleset" , rfreq = [("standard", 100)]- -- Check whether one location is accessible from another.- -- Precondition: the two locations are next to each other.+ -- 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 sloc src tloc tgt ->+ , raccessible = \ lxsize spos src tpos tgt -> F.Walkable `elem` tfeature tgt && not ((F.Closable `elem` tfeature src || F.Closable `elem` tfeature tgt)- && diagonal lxsize (towards lxsize sloc tloc))+ && diagonal lxsize (displacement spos tpos)) , rtitle = "LambdaHack" , rpathsDataFile = Self.getDataFileName , rpathsVersion = Self.version@@ -72,11 +71,8 @@ , rmainMenuArt = [multiline| ---------------------------------------------------------------------------------- | |-| | | >> LambdaHack << | | |-| |-| | | {{{{{{{{{{{{{{{{{{{{{{{{{ | | | | {{{{{{{{{{{{{{{{{{{{{{{{{ |@@ -87,10 +83,13 @@ | | | {{{{{{{{{{{{{{{{{{{{{{{{{ | | |-| |+| {{{{{{{{{{{{{{{{{{{{{{{{{ | | |+| {{{{{{{{{{{{{{{{{{{{{{{{{ | | |+| {{{{{{{{{{{{{{{{{{{{{{{{{ | | |+| {{{{{{{{{{{{{{{{{{{{{{{{{ | | | | | | |
LambdaHack/Content/StrategyKind.hs view
@@ -2,12 +2,12 @@ -- | AI strategies for LambdaHack. module Content.StrategyKind ( cdefs ) where -import Game.LambdaHack.Ability-import Game.LambdaHack.CDefs+import Game.LambdaHack.Common.Ability+import Game.LambdaHack.Common.ContentDef import Game.LambdaHack.Content.StrategyKind -cdefs :: CDefs StrategyKind-cdefs = CDefs+cdefs :: ContentDef StrategyKind+cdefs = ContentDef { getSymbol = ssymbol , getName = sname , getFreq = sfreq@@ -35,14 +35,14 @@ { ssymbol = '@' , sname = "meleeAdjacent" , sfreq = [("meleeAdjacent", 1)]- , sabilities = [Melee, Track]+ , sabilities = [Track, Melee] } meleeAndRanged = StrategyKind -- melee and reaction fire { ssymbol = '@' , sname = "meleeAndRanged" , sfreq = [("meleeAndRanged", 1)]- , sabilities = [Melee, Ranged, Track]+ , sabilities = [Track, Melee, Ranged] } fullAbility = StrategyKind
LambdaHack/Content/TileKind.hs view
@@ -2,23 +2,22 @@ -- | Terrain tiles for LambdaHack. module Content.TileKind ( cdefs ) where -import Game.LambdaHack.Color-import Game.LambdaHack.CDefs-import qualified Game.LambdaHack.Effect as Effect-import Game.LambdaHack.Feature+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-import qualified Game.LambdaHack.Random as Random -cdefs :: CDefs TileKind-cdefs = CDefs+cdefs :: ContentDef TileKind+cdefs = ContentDef { getSymbol = tsymbol , getName = tname , getFreq = tfreq , validate = tvalidate , content =- [wall, pillar, wallV, doorHiddenV, doorClosedV, doorOpenV, wallH, doorHiddenH, doorClosedH, doorOpenH, stairsUpDark, stairsUpLit, stairsDownDark, stairsDownLit, unknown, floorCorridorLit, floorCorridorDark, floorArenaLit, floorArenaDark, floorRoomLit, floorRoomDark, floorRed, floorBlue, floorGreen, floorBrown]+ [wall, hardRock, pillar, wallV, wallSuspectV, doorClosedV, doorOpenV, wallH, wallSuspectH, doorClosedH, doorOpenH, stairsUpDark, stairsUpLit, stairsDownDark, stairsDownLit, escapeUpDark, escapeUpLit, unknown, floorCorridorLit, floorCorridorDark, floorArenaLit, floorArenaDark, floorItemLit, floorItemDark, floorActorItemLit, floorActorItemDark, floorRed, floorBlue, floorGreen, floorBrown] }-wall, pillar, wallV, doorHiddenV, doorClosedV, doorOpenV, wallH, doorHiddenH, doorClosedH, doorOpenH, stairsUpDark, stairsUpLit, stairsDownDark, stairsDownLit, unknown, floorCorridorLit, floorCorridorDark, floorArenaLit, floorArenaDark, floorRoomLit, floorRoomDark, floorRed, floorBlue, floorGreen, floorBrown :: TileKind+wall, hardRock, pillar, wallV, wallSuspectV, doorClosedV, doorOpenV, wallH, wallSuspectH, doorClosedH, doorOpenH, stairsUpDark, stairsUpLit, stairsDownDark, stairsDownLit, escapeUpDark, escapeUpLit, unknown, floorCorridorLit, floorCorridorDark, floorArenaLit, floorArenaDark, floorItemLit, floorItemDark, floorActorItemLit, floorActorItemDark, floorRed, floorBlue, floorGreen, floorBrown :: TileKind wall = TileKind { tsymbol = ' '@@ -28,26 +27,39 @@ , tcolor2 = defFG , tfeature = [] }+hardRock = TileKind+ { tsymbol = ' '+ , tname = "hard rock"+ , tfreq = [("hard rock", 1)]+ , tcolor = BrBlack+ , tcolor2 = BrBlack+ , tfeature = [Impenetrable]+ } pillar = TileKind { tsymbol = 'O' , tname = "pillar"- , tfreq = [("litLegend", 100), ("darkLegend", 100), ("noiseSet", 55)]+ , tfreq = [ ("litLegend", 100), ("darkLegend", 100)+ , ("noiseSet", 55), ("combatSet", 5) ] , tcolor = BrWhite , tcolor2 = defFG , tfeature = [] } wallV = TileKind { tsymbol = '|'- , tname = "wall"+ , tname = "granite wall" , tfreq = [("litLegend", 100), ("darkLegend", 100)] , tcolor = BrWhite , tcolor2 = defFG- , tfeature = []+ , tfeature = [HiddenAs "suspect vertical wall"] }-doorHiddenV = wallV- { tfreq = [("hidden", 100)]- , tfeature = [ Hidden, Secret (Random.RollDice 7 2)- , ChangeTo "vertical closed door"+wallSuspectV = TileKind+ { tsymbol = '|'+ , tname = "moldy wall"+ , tfreq = [("suspect vertical wall", 1)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [ Suspect+ , ChangeTo "vertical closed door" -- never triggered, hack 47 ] } doorClosedV = TileKind@@ -58,6 +70,7 @@ , tcolor2 = BrBlack , tfeature = [ Exit, Openable , ChangeTo "vertical open door"+ , HiddenAs "suspect vertical wall" ] } doorOpenV = TileKind@@ -72,16 +85,20 @@ } wallH = TileKind { tsymbol = '-'- , tname = "wall"+ , tname = "granite wall" , tfreq = [("litLegend", 100), ("darkLegend", 100)] , tcolor = BrWhite , tcolor2 = defFG- , tfeature = []+ , tfeature = [HiddenAs "suspect horizontal wall"] }-doorHiddenH = wallH- { tfreq = [("hidden", 100)]- , tfeature = [ Hidden, Secret (Random.RollDice 7 2)- , ChangeTo "horizontal closed door"+wallSuspectH = TileKind+ { tsymbol = '-'+ , tname = "scratched wall"+ , tfreq = [("suspect horizontal wall", 1)]+ , tcolor = BrWhite+ , tcolor2 = defFG+ , tfeature = [ Suspect+ , ChangeTo "horizontal closed door" -- never triggered, hack 47 ] } doorClosedH = TileKind@@ -92,6 +109,7 @@ , tcolor2 = BrBlack , tfeature = [ Exit, Openable , ChangeTo "horizontal open door"+ , HiddenAs "suspect horizontal wall" ] } doorOpenH = TileKind@@ -113,7 +131,7 @@ -- Dark room interior, OTOH, is fine: , tcolor = BrWhite , tcolor2 = BrBlack- , tfeature = [Walkable, Clear, Exit, Ascendable, Cause Effect.Ascend]+ , tfeature = [Walkable, Clear, Exit, Ascendable, Cause $ Effect.Ascend 1] } stairsUpLit = stairsUpDark { tfreq = [("litLegend", 100)]@@ -130,7 +148,7 @@ -- Dark room interior, OTOH, is fine: , tcolor = BrWhite , tcolor2 = BrBlack- , tfeature = [Walkable, Clear, Exit, Descendable, Cause Effect.Descend]+ , tfeature = [Walkable, Clear, Exit, Descendable, Cause $ Effect.Descend 1] } stairsDownLit = stairsDownDark { tfreq = [("litLegend", 100)]@@ -138,6 +156,18 @@ , 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+ } unknown = TileKind { tsymbol = ' ' , tname = "unknown space"@@ -164,7 +194,7 @@ floorArenaLit = floorCorridorLit { tsymbol = '.' , tname = "stone floor"- , tfreq = [("noiseSet", 100), ("floorArenaLit", 1)]+ , tfreq = [("floorArenaLit", 1), ("noiseSet", 100), ("combatSet", 100)] } floorArenaDark = floorCorridorDark { tsymbol = '.'@@ -175,13 +205,21 @@ -- Dark room interior, OTOH, is fine: , tcolor2 = BrBlack }-floorRoomLit = floorArenaLit+floorItemLit = floorArenaLit+ { tfreq = [("combatSet", 100)]+ , tfeature = CanItem : tfeature floorCorridorLit+ }+floorItemDark = floorArenaDark+ { tfreq = []+ , tfeature = CanItem : tfeature floorCorridorDark+ }+floorActorItemLit = floorItemLit { tfreq = [("litLegend", 100), ("floorRoomLit", 1)]- , tfeature = Boring : tfeature floorArenaLit+ , tfeature = CanActor : tfeature floorItemLit }-floorRoomDark = floorArenaDark+floorActorItemDark = floorItemDark { tfreq = [("darkLegend", 100)]- , tfeature = Boring : tfeature floorArenaDark+ , tfeature = CanActor : tfeature floorItemDark } floorRed = floorArenaLit { tname = "brick pavement"
LambdaHack/Main.hs view
@@ -1,9 +1,9 @@+{-# OPTIONS_GHC -fno-warn-orphans #-} -- | The main 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 Game.LambdaHack.Kind as Kind import qualified Content.ActorKind import qualified Content.CaveKind import qualified Content.FactionKind@@ -12,16 +12,26 @@ import qualified Content.RuleKind import qualified Content.StrategyKind import qualified Content.TileKind-import Game.LambdaHack.Turn-import Game.LambdaHack.Action-import Game.LambdaHack.BindingAction+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 --- | Fire up the frontend with the engine fueled by content.--- Which of the frontends is run depends on the flags supplied--- when compiling the engine library.+instance MonadAtomic ActionSer where+ execAtomic = atomicSendSem++instance MonadAtomic (ActionCli c) where+ execAtomic (CmdAtomic cmd) = cmdAtomicSem cmd+ execAtomic (SfxAtomic _) = return ()+ main :: IO () main =- let cops = Kind.COps+ let copsSlow = Kind.COps { coactor = Kind.createOps Content.ActorKind.cdefs , cocave = Kind.createOps Content.CaveKind.cdefs , cofact = Kind.createOps Content.FactionKind.cdefs@@ -31,4 +41,4 @@ , costrat = Kind.createOps Content.StrategyKind.cdefs , cotile = Kind.createOps Content.TileKind.cdefs }- in startFrontend cops stdBinding handleTurn+ in mainSer copsSlow executorSer $ exeFrontend executorCli executorCli
PLAYING.md view
@@ -14,16 +14,18 @@ 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 symbol '@' and with '1', '2', ..., '9'.+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 consists of 10 levels and each level consists of 80 by 21 tiles.-The basic tiles are as follows.+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 .@@ -37,13 +39,7 @@ rock 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. Some items-aid in dungeon exploration, e.g., a ring of searching improves the speed-of finding hidden doors by heroes and monsters. The higher the ability-bonus displayed for this and other dungeon items, the more effective it is.-Only the best item carried in a hero's or monster's inventory counts.-You can throw the rest away, but beware that your adversaries may pick it up-and use it against your party.+during a single game, the level layout is the same. Keys@@ -59,69 +55,74 @@ 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. '5' and '.' use a turn-to brace for combat, which gives a chance to block blows next turn.+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 next 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 *.-- key command- < ascend a level*- > descend a level*- ? display help- R restart game*- S save game- X save and exit*- c close a door*- d drop an object*- g get an object*- i display inventory- o open a door*- q quaff a potion*- r read a scroll*- t throw a dart*- z zap a wand*+hero time are marked with a star. -To make a ranged attack, you need to set your target first, using-targeting mode. Note that the target, for the few commands that require any,-is indicated by the targeting cursor. 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.+ 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 on a different level+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+ ESC cancel action or bring up the Main Menu RET accept choice- SPACE clear messages- TAB cycle among heroes on level+ TAB cycle among heroes on the level SHIFT-TAB cycle among heroes in the dungeon * target monster + swerve targeting line - unswerve targeting line- / target location- D dump current configuration+ / 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 -There are also some debug and cheat keys, all entered with the CTRL-key modifier. Use at your own peril!+Commands for saving 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-o toggle "omniscience"- CTRL-i inform about level meta-data- CTRL-r rotate vision modes (effective next turn)+ key command+ CTRL-s save game+ 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 and cheat options that can be specified+on the command line when starting the game server. Use at your own peril! + Monsters -------- @@ -136,8 +137,8 @@ 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 another (with the Shift key) do not inflict damage, but change places.+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. @@ -153,7 +154,8 @@ On Winning and Dying -------------------- -You win the game if you escape the dungeon alive. Your score is+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).
README.md view
@@ -1,4 +1,4 @@-LambdaHack+LambdaHack [](http://travis-ci.org/kosmikus/LambdaHack) ========== This is an alpha release of LambdaHack, a [Haskell] [1] game engine@@ -10,23 +10,23 @@ or a custom-made main loop. Several frontends are available (GTK is the default) and many other generic engine components are easily overridden, but the fundamental source of flexibility lies-in the strict and type-safe separation of code and content.-Long-term goals for LambdaHack include support for tactical squad combat,-in-game content creation, auto-balancing and persistent content-modification based on player behaviour.+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. The engine comes with a sample code for a little dungeon crawler, 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 game by modifying the sample game+You are welcome to create your own games by modifying the sample game and the engine code, but please consider eventually splitting your changes into a separate Hackage package that depends on the upstream library, to help us exchange ideas and share improvements to the common code. Games known to use the LambdaHack library: -* Allure of the Stars, a near-future Sci-Fi game in early development,-see http://hackage.haskell.org/package/Allure+* [Allure of the Stars] [6], a near-future Sci-Fi game in early development Compilation and installation@@ -43,29 +43,18 @@ cabal install -For the example game, the best frontend (keyboard support and colours) is gtk.-To compile with one of the terminal frontends, use Cabal flags, e.g,+For the example game, the best frontend (wrt keyboard support and colours)+is the default gtk. To compile with one of the terminal frontends,+use Cabal flags, e.g, cabal install -fvty -To use a crude bot for testing the game, you have to compile with-the standard input/output frontend, as follows - cabal install -fstd--and run the bot, for instance storing the output in a log-- DumbBot 42 20000000 | LambdaHack > /tmp/log--You may wish to tweak the game configuration file for the bot,-e.g., by helping it play longer, as in the supplied config.rules.bot.-- Compatibility notes ------------------- -The current code was tested with GHC 7.6.1, but probably works with GHC > 7.2.-A [few tweaks] [6] are needed to compile with 7.0.+The current code was tested with GHC 7.6, but should also work with+other GHC versions (see file .travis.yml for GHC 7.4 commands). If you are using the curses or vty frontends, numerical keypad may not work correctly depending on the versions@@ -92,4 +81,4 @@ [3]: http://hackage.haskell.org/package/LambdaHack [4]: https://github.com/kosmikus/LambdaHack/wiki [5]: http://github.com/kosmikus/LambdaHack-[6]: https://github.com/Mikolaj/Allure/commit/3d0aa5bef7a0ef39e7611d4e12229224f4cead75+[6]: http://hackage.haskell.org/package/Allure
− config.rules.bot
@@ -1,9 +0,0 @@-; Overriding config file options to help the bot.-; DumbBot 42 20000000 | LambdaHack > /tmp/log--[dungeon]-depth: 100--[heroes]-baseHP: 999999-extraHeroes: 1
config.rules.default view
@@ -4,27 +4,45 @@ ; ; ~/.LambdaHack/config.rules.ini (or a similar path, depending on the OS). ; ; Warning: options are case-sensitive and only ';' for comments is permitted. -; [dungeon]-; depth: 10- ; [caves]+; ; Cave "dng" means a random choice from caves that can randomly appear;+; ; this is the default and the lack of the Escape feature is the default. ; ; Fixed caves for the first levels, then randomly picked caves.-; LambdaCave_1: caveRogue-; LambdaCave_2: caveRogue-; LambdaCave_3: caveEmpty-; LambdaCave_10: caveNoise+; campaign: [(1, ("caveRogue", True)), (2, ("caveRogue", False)), (3, ("caveEmpty", False)), (10, ("caveNoise", False))]+; combat: [(3, ("caveCombat", False))]+; defense: [(1, ("dng", False)), (5, ("caveEmpty", True))] ; [engine]+; ;dungeonRandomGenerator: 42+; firstDeathEnds: False ; fovMode: Digital 12 ; ;fovMode: Permissive ; ;fovMode: Shadow ; ;startingRandomGenerator: 42-; ;dungeonRandomGenerator: 42-; smellTimeout: 100+; saveBkpClips: 100 -; [heroes]-; baseHP: 50-; extraHeroes: 2-; firstDeathEnds: False-; ; faction "playable" means a random choice-; faction: hero+; [file]+; ; Names of various game files. They reside in ~/.LambdaHack or similar.+; 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++; [players]+; campaign: Players { playersHuman = [Player {playerName = "Adventuring Party", playerKind = "hero", playerInitial = 1, playerEntry = LevelId 1}], playersComputer = [Player {playerName = "Monster Hive", playerKind = "monster", playerInitial = 5, playerEntry = LevelId 3}], playersEnemy = [("Adventuring Party", "Monster Hive")], playersAlly = [] }+; skirmish: Players { playersHuman = [Player {playerName = "White", playerKind = "hero", playerInitial = 3, playerEntry = LevelId 1}], playersComputer = [Player {playerName = "Green", playerKind = "hero", playerInitial = 3, playerEntry = LevelId 1}, Player {playerName = "Horror Den", playerKind = "horror", playerInitial = 0, playerEntry = LevelId 1}], playersEnemy = [("White", "Green"), ("White", "Horror Den"), ("Green", "Horror Den")], playersAlly = [] }+; PvP: Players { playersHuman = [Player {playerName = "Red", playerKind = "hero", playerInitial = 3, playerEntry = LevelId 1}, Player {playerName = "Blue", playerKind = "hero", playerInitial = 3, playerEntry = LevelId 1}], playersComputer = [Player {playerName = "Horror Den", playerKind = "horror", playerInitial = 0, playerEntry = LevelId 1}], playersEnemy = [("Red", "Blue"), ("Red", "Horror Den"), ("Blue", "Horror Den")], playersAlly = [] }+; Coop: Players { playersHuman = [Player {playerName = "Coral", playerKind = "hero", playerInitial = 1, playerEntry = LevelId 1}, Player {playerName = "Amber", playerKind = "hero", playerInitial = 1, playerEntry = LevelId 1}], playersComputer = [Player {playerName = "Monster Hive", playerKind = "monster", playerInitial = 5, playerEntry = LevelId 3}], playersEnemy = [("Coral", "Monster Hive"), ("Amber", "Monster Hive")], playersAlly = [("Coral", "Amber")] }+; defense: Players { playersHuman = [Player {playerName = "Monster Hive", playerKind = "monster", playerInitial = 1, playerEntry = LevelId 1}], playersComputer = [Player {playerName = "Green", playerKind = "hero", playerInitial = 1, playerEntry = LevelId 1}, Player {playerName = "Yellow", playerKind = "hero", playerInitial = 2, playerEntry = LevelId 1}, Player {playerName = "Cyan", playerKind = "hero", playerInitial = 3, playerEntry = LevelId 1}], playersEnemy = [("Green", "Monster Hive"), ("Yellow", "Monster Hive"), ("Cyan", "Monster Hive")], playersAlly = [("Green", "Yellow"), ("Green", "Cyan"), ("Yellow", "Cyan")] }++; [scenario]+; campaign: Scenario { scenarioPlayers = "campaign", scenarioDungeon = "campaign" }+; skirmish: Scenario { scenarioPlayers = "skirmish", scenarioDungeon = "combat" }+; PvP: Scenario { scenarioPlayers = "PvP", scenarioDungeon = "combat" }+; Coop: Scenario { scenarioPlayers = "Coop", scenarioDungeon = "campaign" }+; defense: Scenario { scenarioPlayers = "defense", scenarioDungeon = "defense" }
config.ui.default view
@@ -7,60 +7,45 @@ ; [commands] ; ; All commands are defined here, except movement, hero selection and debug. ; ;-; ; Interaction with the dungeon.-; c: TriggerDir { verb = "close", object = "door", feature = Closable }-; o: TriggerDir { verb = "open", object = "door", feature = Openable }-; less: TriggerTile { verb = "ascend", object = "level", feature = Cause Ascend }-; greater: TriggerTile { verb = "descend", object = "level", feature = Cause Descend }+; 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: TriggerDir [BumpFeature {verb = "close", object = "door", feature = Closable}]+; o: TriggerDir [BumpFeature {verb = "open", object = "door", feature = Openable}]+; less: TriggerTile [BumpFeature {verb = "ascend", object = "level", feature = Cause (Ascend 1)}, BumpFeature {verb = "escape", object = "dungeon", feature = Cause Escape}]+; greater: TriggerTile [BumpFeature {verb = "descend", object = "level", feature = Cause (Descend 1)}, BumpFeature {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+; CTRL-d: CfgDump+; Tab: MemberCycle+; ISO_Left_Tab: MemberBack+; I: Inventory+; slash: TgtFloor+; asterisk: TgtEnemy ; bracketleft: TgtAscend 1 ; bracketright: TgtAscend (-1) ; braceleft: TgtAscend 10 ; braceright: TgtAscend (-10)-; slash: TgtFloor-; asterisk: TgtEnemy ; plus: EpsIncr True ; minus: EpsIncr False-; Tab: HeroCycle-; ISO_Left_Tab: HeroBack-; ;-; ; Items.-; g: Pickup-; d: Drop-; i: Inventory-; q: Apply { verb = "quaff", object = "potion", syms = "!" }-; r: Apply { verb = "read", object = "scroll", syms = "?" }-; z: Project { verb = "zap", object = "wand", syms = "/" }-; t: Project { verb = "throw", object = "missile", syms = "|" }-; ;-; ; Saving, exiting and restarting the game.-; X: GameExit-; R: GameRestart-; S: GameSave-; ;-; ; Information for the player.-; P: History-; question: Help-; D: CfgDump-; ;-; ; General.-; KP_Begin: Wait ; Escape: Cancel ; Return: Accept ; space: Clear--; [files]-; ; Names of various game files. They reside in ~/.LambdaHack or similar.-; scoresFile: scores-; saveFile: save-; diaryFile : diary--; [heroNames]-; HeroName_0: you-; HeroName_1: Haskell Alvin-; HeroName_2: Alonzo Barkley-; HeroName_3: Ernst Abraham-; HeroName_4: Samuel Saunders-; HeroName_5: Roger Robin+; P: History+; V: MarkVision+; S: MarkSmell+; T: MarkSuspect+; question: Help ; [macros] ; ; Handy with Vi keys:
scores view
binary file changed (133 → 277 bytes)