packages feed

swarm-0.7.0.0: src/swarm-engine/Swarm/Game/Scenario/Scoring/GenericMetrics.hs

{-# LANGUAGE TemplateHaskell #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Data types and functions applicable across different
-- scoring methods.
module Swarm.Game.Scenario.Scoring.GenericMetrics (
  Progress (..),
  Metric (Metric),
  metricProgress,
  metricData,
  chooseBetter,
) where

import Control.Applicative ((<|>))
import Control.Lens
import Data.Aeson
import Data.List.Extra (dropPrefix)
import Data.Ord (Down (Down))
import GHC.Generics (Generic)
import Swarm.Util (maxOn)
import Swarm.Util.JSON (optionsMinimize, optionsUntagged)
import Swarm.Util.Lens (makeLensesNoSigs)

-- | This is a subset of the "ScenarioStatus" type
-- that excludes the "NotStarted" case.
data Progress
  = Attempted
  | Completed
  deriving (Eq, Ord, Show, Read, Generic)

instance FromJSON Progress where
  parseJSON = genericParseJSON optionsUntagged

instance ToJSON Progress where
  toJSON = genericToJSON optionsUntagged

data Metric a = Metric
  { _metricProgress :: Progress
  , _metricData :: a
  }
  deriving (Eq, Ord, Show, Read, Generic)

metricSerializeOptions :: Options
metricSerializeOptions = optionsMinimize {fieldLabelModifier = camelTo2 '_' . dropPrefix "_metric"}

instance FromJSON a => FromJSON (Metric a) where
  parseJSON v =
    (uncurry Metric <$> parseJSON v) -- parse saves from time when metric did not have named fields
      <|> genericParseJSON metricSerializeOptions v

instance ToJSON a => ToJSON (Metric a) where
  toJSON = genericToJSON metricSerializeOptions

makeLensesNoSigs ''Metric

-- | The player progress, so that we know if this game was completed.
metricProgress :: Lens' (Metric a) Progress

-- | Metric data, for example start and end time.
metricData :: Lens' (Metric a) a

-- | This encodes the notion of "more play is better"
-- for incomplete games (rationale: more play = more fun),
--  whereas "smaller inputs are better" for completed games.
--
-- Since 'Maybe' has its own 'Ord' instance where
-- @Nothing < Just x@ regardless of @x@, when we want to
-- choose the minimum value we @fmap Down@ to ensure that
-- the 'Just' is selected while inverting the ordering of
-- the inner member.
chooseBetter ::
  Ord a =>
  -- | criteria; record field extractor
  (b -> Maybe a) ->
  -- | x
  Metric b ->
  -- | y
  Metric b ->
  Metric b
chooseBetter criteria (Metric Attempted x) (Metric Attempted y) =
  Metric Attempted $ maxOn criteria x y
chooseBetter criteria (Metric Completed x) (Metric Completed y) =
  Metric Completed $ maxOn (fmap Down . criteria) x y
-- Having exhausted the possibilities where either both
-- are Completed or both are Attempted, now we can just
-- choose the Completed one.
chooseBetter _ x@(Metric Completed _) _ = x
chooseBetter _ _ y@(Metric Completed _) = y