packages feed

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

-- | Data types and functions applicable across different
-- scoring methods.
module Swarm.Game.Scenario.Scoring.GenericMetrics where

import Data.Aeson
import Data.Ord (Down (Down))
import GHC.Generics (Generic)
import Swarm.Util (maxOn)

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

untaggedJsonOptions :: Options
untaggedJsonOptions =
  defaultOptions
    { sumEncoding = UntaggedValue
    }

instance FromJSON Progress where
  parseJSON = genericParseJSON untaggedJsonOptions

instance ToJSON Progress where
  toJSON = genericToJSON untaggedJsonOptions

data Metric a = Metric Progress a
  deriving (Eq, Ord, Show, Read, Generic, FromJSON, ToJSON)

getMetric :: Metric a -> a
getMetric (Metric _ x) = x

-- | 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