packages feed

swarm-0.4: src/Swarm/Game/Scenario/Scoring/Best.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Types and records for updating and retrieving
-- the best scores for a scenario.
module Swarm.Game.Scenario.Scoring.Best where

import Control.Arrow ((&&&))
import Control.Lens hiding (from, (<.>))
import Data.Aeson (
  genericParseJSON,
  genericToEncoding,
  genericToJSON,
 )
import Data.Function (on)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Map (Map)
import Data.Map qualified as M
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Time (ZonedTime, zonedTimeToUTC)
import Data.Yaml as Y
import GHC.Generics (Generic)
import Swarm.Game.Scenario.Scoring.CodeSize
import Swarm.Game.Scenario.Scoring.ConcreteMetrics
import Swarm.Game.Scenario.Scoring.GenericMetrics
import Swarm.Util.Lens (makeLensesNoSigs)

-- * Some orphan ZonedTime instances

instance Eq ZonedTime where
  (==) = (==) `on` zonedTimeToUTC

instance Ord ZonedTime where
  (<=) = (<=) `on` zonedTimeToUTC

-- * High scores by various criteria

data BestByCriteria
  = BestByTime
  | BestByTicks
  | BestByCharCount
  | BestByAstSize
  deriving (Eq, Ord, Bounded, Enum, Show)

describeCriteria :: BestByCriteria -> Text
describeCriteria = \case
  BestByTime -> "time"
  BestByTicks -> "ticks"
  BestByCharCount -> "char count"
  BestByAstSize -> "AST size"

data ProgressStats = ProgressStats
  { _scenarioStarted :: ZonedTime
  -- ^ Time when the scenario was started including time zone.
  , _scenarioAttemptMetrics :: AttemptMetrics
  }
  deriving (Eq, Ord, Show, Read, Generic)

makeLenses ''ProgressStats

instance FromJSON ProgressStats where
  parseJSON = genericParseJSON scenarioOptions

instance ToJSON ProgressStats where
  toEncoding = genericToEncoding scenarioOptions
  toJSON = genericToJSON scenarioOptions

type ProgressMetric = Metric ProgressStats

-- * High scores by various criteria

data BestRecords = BestRecords
  { _scenarioBestByTime :: ProgressMetric
  , _scenarioBestByTicks :: ProgressMetric
  , _scenarioBestByCharCount :: ProgressMetric
  , _scenarioBestByAstSize :: ProgressMetric
  }
  deriving (Eq, Ord, Read, Generic)

instance Show BestRecords where
  show (BestRecords a b c d) =
    unlines $
      map
        angleBracket
        [ show a
        , show b
        , show c
        , show d
        ]
   where
    angleBracket :: String -> String
    angleBracket x = "<" <> x <> ">"

emptyBest :: ZonedTime -> BestRecords
emptyBest t = BestRecords x x x x
 where
  x = Metric Attempted $ ProgressStats t emptyAttemptMetric

updateBest :: ProgressMetric -> BestRecords -> BestRecords
updateBest newPlayMetric (BestRecords oldA oldB oldC oldD) =
  BestRecords
    (bestTime oldA scenarioElapsed)
    (bestTime oldB scenarioElapsedTicks)
    (bestSize oldC sourceTextLength)
    (bestSize oldD astSize)
 where
  f x y = chooseBetter y newPlayMetric x
  bestTime x y = f x (Just . view (scenarioAttemptMetrics . scenarioDurationMetrics . y))
  bestSize x y = f x (fmap y . view (scenarioAttemptMetrics . scenarioCodeMetrics))

makeLensesNoSigs ''BestRecords

-- | The best status of the scenario, measured in real world time.
scenarioBestByTime :: Lens' BestRecords ProgressMetric

-- | The best status of the scenario, measured in game ticks.
scenarioBestByTicks :: Lens' BestRecords ProgressMetric

-- | The best code size of the scenario, measured in character count.
scenarioBestByCharCount :: Lens' BestRecords ProgressMetric

-- | The best code size of the scenario, measured in AST size.
scenarioBestByAstSize :: Lens' BestRecords ProgressMetric

instance FromJSON BestRecords where
  parseJSON = genericParseJSON scenarioOptions

instance ToJSON BestRecords where
  toEncoding = genericToEncoding scenarioOptions
  toJSON = genericToJSON scenarioOptions

-- | Uses the start time of the play-attempt to de-dupe
-- records that are from the same game. The start time should
-- be sufficient to uniquely identify a game.
getBestGroups ::
  BestRecords ->
  [(Metric ProgressStats, NonEmpty BestByCriteria)]
getBestGroups =
  rearrangeTuples . M.toList . bestToMap
 where
  groupByStartTime = NE.groupAllWith $ view scenarioStarted . getMetric . snd
  rearrangeTuples = map (snd . NE.head &&& NE.map fst) . groupByStartTime

  bestToMap :: BestRecords -> Map BestByCriteria ProgressMetric
  bestToMap (BestRecords t1 t2 s1 s2) =
    M.fromList $ durationElements <> mapMaybe (traverse ensurePresent) codeSizeElements
   where
    durationElements =
      [ (BestByTime, t1)
      , (BestByTicks, t2)
      ]
    codeSizeElements =
      [ (BestByCharCount, s1)
      , (BestByAstSize, s2)
      ]

    ensurePresent x =
      (getMetric x ^. scenarioAttemptMetrics . scenarioCodeMetrics) >> Just x