{-# LANGUAGE DeriveGeneric #-} -- | The type of kinds of game modes. module Game.LambdaHack.Content.ModeKind ( Caves, Roster(..), Player(..), ModeKind(..), Tactic(..) , LeaderMode(..), AutoLeader(..) , validateSingleModeKind, validateAllModeKind ) where import Data.Binary import Data.Hashable (Hashable) import qualified Data.IntMap.Strict as IM import Data.Text (Text) import qualified Data.Text as T import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU () import Game.LambdaHack.Common.Ability import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Msg -- | Game mode specification. data ModeKind = ModeKind { msymbol :: !Char -- ^ a symbol (matches the keypress, if any) , mname :: !Text -- ^ short description , mfreq :: !Freqs -- ^ frequency within groups , mroster :: !Roster -- ^ players taking part in the game , mcaves :: !Caves -- ^ arena of the game , mdesc :: !Text -- ^ description } deriving Show -- | Requested cave groups for particular levels. The second component -- is the @Escape@ feature on the level. @True@ means it's represented -- by @<@, @False@, by @>@. type Caves = IM.IntMap (GroupName, Maybe Bool) -- | The specification of players for the game mode. data Roster = Roster { rosterList :: ![Player] -- ^ players in the particular team , rosterEnemy :: ![(Text, Text)] -- ^ the initial enmity matrix , rosterAlly :: ![(Text, Text)] -- ^ the initial aliance matrix } deriving (Show, Eq) -- | Properties of a particular player. data Player = Player { fname :: !Text -- ^ name of the player , fgroup :: !GroupName -- ^ name of the monster group to control , fskillsOther :: !Skills -- ^ skills of the other actors , fcanEscape :: !Bool -- ^ the player can escape the dungeon , fneverEmpty :: !Bool -- ^ the faction declared killed if no actors , fhasNumbers :: !Bool -- ^ whether actors have numbers, not symbols , fhasGender :: !Bool -- ^ whether actors have gender , ftactic :: !Tactic -- ^ members behave according to this tactic , fentryLevel :: !Int -- ^ level where the initial members start , finitialActors :: !Int -- ^ number of initial members , fleaderMode :: !LeaderMode -- ^ the mode of switching the leader , fhasUI :: !Bool -- ^ does the faction have a UI client -- (for control or passive observation) } deriving (Show, Eq, Generic) instance Binary Player -- Keep this type here, so that Contents/ is as self-contained as possible. -- TODO: alway shoot, never shoot, etc., but there is too many and this is best -- expressed via skills, and also we risk micromanagement, so let's stop -- and think first; perhaps only have as many tactics as needed for realistic -- AI behaviour in our game modes; perhaps even expose only some of them to UI data Tactic = TBlock -- ^ always only wait, even if enemy in melee range | TFollow -- ^ always follow leader's target or his position if no target | TExplore -- ^ if enemy nearby, attack, if no items, etc., explore unknown | TRoam -- ^ if enemy nearby, attack, if no items, etc., roam randomly | TPatrol -- ^ find an open and uncrowded area, patrol it according -- to sight radius and fallback temporarily to @TRoam@ -- when enemy is seen by the faction and is within -- the actor's sight radius -- TODO (currently the same as TExplore; should it chase -- targets too (TRoam) and only switch to TPatrol when none?) deriving (Eq, Ord, Enum, Bounded, Generic) instance Show Tactic where show TBlock = "block and wait" show TFollow = "follow leader's target or position" show TExplore = "explore unknown, chase targets" show TRoam = "roam freely, chase targets" show TPatrol = "find and patrol an area (TODO)" instance Binary Tactic instance Hashable Tactic -- | If a faction with @LeaderUI@ and @LeaderAI@ has any actor, it has a leader. data LeaderMode = LeaderNull -- ^ faction can have no leader, is whole under AI control | LeaderAI AutoLeader -- ^ leader under AI control | LeaderUI AutoLeader -- ^ leader under UI control, assumes @fhasUI@ deriving (Show, Eq, Generic) instance Binary LeaderMode data AutoLeader = AutoLeader { autoDungeon :: !Bool -- ^ leader change between levels only automatic , autoLevel :: !Bool -- ^ leader change within a level only automatic -- (for UI, currently no leader change here) } deriving (Show, Eq, Generic) instance Binary AutoLeader -- TODO: (spans multiple contents) Check that caves with the given groups exist. -- | Catch invalid game mode kind definitions. validateSingleModeKind :: ModeKind -> [Text] validateSingleModeKind ModeKind{..} = [ "mname longer than 20" | T.length mname > 20 ] ++ validateSingleRoster mcaves mroster -- TODO: if the diplomacy system stays in, check no teams are at once -- in war and alliance, taking into account symmetry (but not transitvity) -- | Checks, in particular, that there is at least one faction with fneverEmpty -- or the game could get stuck when the dungeon is devoid of actors validateSingleRoster :: Caves -> Roster -> [Text] validateSingleRoster caves Roster{..} = [ "no player keeps the dungeon alive" | all (not . fneverEmpty) rosterList ] ++ concatMap (validateSinglePlayer caves) rosterList ++ let checkPl field pl = [ pl <+> "is not a player name in" <+> field | all ((/= pl) . fname) rosterList ] checkDipl field (pl1, pl2) = [ "self-diplomacy in" <+> field | pl1 == pl2 ] ++ checkPl field pl1 ++ checkPl field pl2 in concatMap (checkDipl "rosterEnemy") rosterEnemy ++ concatMap (checkDipl "rosterAlly") rosterAlly -- Note that @fSkillsOther@ needn't be a subset of @fSkillsLeader@. validateSinglePlayer :: Caves -> Player -> [Text] validateSinglePlayer caves Player{..} = [ "fname empty:" <+> fname | T.null fname ] ++ [ "first word of fname longer than 15:" <+> fname | T.length (head $ T.words fname) > 15 ] ++ [ "no UI client, but UI leader:" <+> fname | not fhasUI && case fleaderMode of LeaderUI _ -> True _ -> False ] ++ [ "fentryLevel not among caves:" <+> fname | fentryLevel `notElem` IM.keys caves ] -- | Validate all game mode kinds. Currently always valid. validateAllModeKind :: [ModeKind] -> [Text] validateAllModeKind _ = []