{-# LANGUAGE DeriveGeneric #-} -- | The type of kinds of game modes. module Game.LambdaHack.Content.ModeKind ( Caves, Roster(..), Player(..), ModeKind(..), LeaderMode(..), AutoLeader(..) , Outcome(..), HiIndeterminant(..), HiCondPoly, HiSummand, HiPolynomial , validateSingleModeKind, validateAllModeKind ) where import Data.Binary import qualified Data.EnumMap.Strict as EM 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 qualified Game.LambdaHack.Common.Dice as Dice import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Msg import Game.LambdaHack.Content.CaveKind import Game.LambdaHack.Content.ItemKind (ItemKind) -- | Game mode specification. data ModeKind = ModeKind { msymbol :: !Char -- ^ a symbol (matches the keypress, if any) , mname :: !Text -- ^ short description , mfreq :: !(Freqs ModeKind) -- ^ 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 CaveKind, Maybe Bool) -- | The specification of players for the game mode. data Roster = Roster { rosterList :: ![Player Dice.Dice] -- ^ players in the particular team , rosterEnemy :: ![(Text, Text)] -- ^ the initial enmity matrix , rosterAlly :: ![(Text, Text)] -- ^ the initial aliance matrix } deriving (Show, Eq) -- | 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, Enum, Bounded, Generic) instance Binary Outcome data HiIndeterminant = HiConst | HiLoot | HiBlitz | HiSurvival | HiKill | HiLoss deriving (Show, Eq, Ord, Generic) instance Binary HiIndeterminant type HiPolynomial = [(HiIndeterminant, Double)] type HiSummand = (HiPolynomial, [Outcome]) -- | Conditional polynomial representing score calculation for this player. type HiCondPoly = [HiSummand] -- | Properties of a particular player. data Player a = Player { fname :: !Text -- ^ name of the player , fgroup :: !(GroupName ItemKind) -- ^ name of the monster group to control , fskillsOther :: !Skills -- ^ fixed skill modifiers to the non-leader -- actors; also summed with skills implied -- by ftactic (which is not fixed) , fcanEscape :: !Bool -- ^ the player can escape the dungeon , fneverEmpty :: !Bool -- ^ the faction declared killed if no actors , fhiCondPoly :: !HiCondPoly -- ^ score polynomial for the player , fhasNumbers :: !Bool -- ^ whether actors have numbers, not symbols , fhasGender :: !Bool -- ^ whether actors have gender , ftactic :: !Tactic -- ^ non-leader behave according to this -- tactic; can be changed during the game , fentryLevel :: !a -- ^ level where the initial members start , finitialActors :: !a -- ^ 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, Ord, Generic) instance Binary a => Binary (Player a) -- | 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, Ord, Generic) instance Binary LeaderMode data AutoLeader = AutoLeader { autoDungeon :: !Bool -- ^ leader switching between levels is automatically done by the server -- and client is not permitted to change to leaders from other levels -- (the frequency of leader level switching done by the server -- is controlled by @RuleKind.rleadLevelClips@); -- if the flag is @False@, server still does a subset -- of the automatic switching, e.g., when the old leader dies -- and no other actor of the faction resides on his level, -- but the client (particularly UI) is expected to do changes as well , autoLevel :: !Bool -- ^ leader switching within a level is automatically done by the server -- and client is not permitted to change leaders -- (server is guaranteed to switch leader within a level very rarely, -- e.g., when the old leader dies); -- if the flag is @False@, server still does a subset -- of the automatic switching, but the client is permitted to do more } deriving (Show, Eq, Ord, 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 validateSinglePlayer :: Caves -> Player Dice.Dice -> [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 value not among cave numbers:" <+> fname | any (`notElem` IM.keys caves) [Dice.minDice fentryLevel .. Dice.maxDice fentryLevel] ] -- simplification ++ [ "fskillsOther not negative:" <+> fname | any (>= 0) $ EM.elems fskillsOther ] -- | Validate all game mode kinds. Currently always valid. validateAllModeKind :: [ModeKind] -> [Text] validateAllModeKind _ = []