packages feed

crem-0.1.0.0: examples/Crem/Example/Uno.hs

{-# LANGUAGE DataKinds #-}
{-# HLINT ignore "Redundant id" #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- https://downloads.haskell.org/ghc/latest/docs/users_guide/using-warnings.html#ghc-flag--Wmissing-deriving-strategies
{-# OPTIONS_GHC -Wno-missing-deriving-strategies #-}
-- https://downloads.haskell.org/ghc/latest/docs/users_guide/using-warnings.html#ghc-flag--Wunrecognised-pragmas
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
-- https://downloads.haskell.org/ghc/latest/docs/users_guide/using-warnings.html#ghc-flag--Wunticked-promoted-constructors
{-# OPTIONS_GHC -Wno-unticked-promoted-constructors #-}
-- https://downloads.haskell.org/ghc/latest/docs/users_guide/using-warnings.html#ghc-flag--Wunused-type-patterns
{-# OPTIONS_GHC -Wno-unused-type-patterns #-}

-- | Porting of https://github.com/thinkbeforecoding/UnoCore/blob/solution/Uno/Game.fs
module Crem.Example.Uno where

import Crem.BaseMachine (InitialState (..))
import Crem.Decider (Decider (..), EvolutionResult (..))
import Crem.Render.RenderableVertices (AllVertices (..), RenderableVertices)
import Crem.Topology
import "singletons-base" Data.Singletons.Base.TH
import Prelude hiding (id, init, reverse)
import Prelude qualified (id)

-- * Domain

newtype PlayerCount = PlayerCount Int
  deriving newtype (Eq, Ord, Show)

data Digit
  = Zero
  | One
  | Two
  | Three
  | Four
  | Five
  | Six
  | Seven
  | Eigth
  | Nine
  deriving stock (Eq, Show)

data Colour
  = Red
  | Green
  | Blue
  | Yellow
  deriving stock (Eq, Show)

data Card
  = DigitCard Digit Colour
  | Skip Colour
  | Kickback Colour
  deriving stock (Eq, Show)

colour :: Card -> Colour
colour (DigitCard _ c) = c
colour (Skip c) = c
colour (Kickback c) = c

sameColour :: Card -> Card -> Bool
sameColour card1 card2 =
  colour card1 == colour card2
data CardValue
  = DigitValue Digit
  | SkipValue
  | KickbackValue
  deriving stock (Eq)

value :: Card -> CardValue
value (DigitCard d _) = DigitValue d
value (Skip _) = SkipValue
value (Kickback _) = KickbackValue

sameValue :: Card -> Card -> Bool
sameValue card1 card2 =
  value card1 == value card2

newtype PlayerId = PlayerId Int
  deriving newtype (Eq, Show)

data Direction
  = Clockwise
  | CounterClockwise

reverseDirection :: Direction -> Direction
reverseDirection Clockwise = CounterClockwise
reverseDirection CounterClockwise = Clockwise

data Player = Player
  { id :: PlayerId
  , count :: PlayerCount
  , direction :: Direction
  }

init :: PlayerId -> PlayerCount -> Player
init playerId count =
  Player
    { id = playerId
    , count = count
    , direction = Clockwise
    }

next :: Player -> Player
next (Player (PlayerId id) (PlayerCount count) direction) =
  case direction of
    Clockwise ->
      Player
        { id = PlayerId $ (id + 1) `mod` count
        , count = PlayerCount count
        , direction
        }
    CounterClockwise ->
      Player
        { id = PlayerId $ (id - 1) `mod` count
        , count = PlayerCount count
        , direction
        }

skip :: Player -> Player
skip = next . next

reverse :: Player -> Player
reverse (Player id count direction) =
  Player id count $ reverseDirection direction

nextPlayer :: Card -> Player -> Player
nextPlayer card player = case card of
  DigitCard _ _ -> next player
  Skip _ -> skip player
  Kickback _ -> next . reverse $ player

set :: PlayerId -> Player -> Player
set playerId player = player {id = playerId}

-- * Commands and events

data InitialData = InitialData
  { players :: PlayerCount
  , firstCard :: Card
  }
  deriving stock (Eq, Show)

data PlayData = PlayData
  { playerId :: PlayerId
  , card :: Card
  }
  deriving stock (Eq, Show)

data Command
  = StartGame InitialData
  | PlayCard PlayData
  deriving stock (Show)

-- deriving (Arbitrary) via (GenericArbitrary Command)

data Event
  = GameStarted InitialData PlayerId
  | CardPlayed PlayData
  | CardPlayedAndTurnBegan PlayData PlayerId
  | WrongCardPlayed PlayData
  | PlayerPlayedAtWrongTurn PlayData
  deriving stock (Eq, Show)

-- * Topology

$( singletons
    [d|
      data UnoVertex
        = Initial
        | Started
        deriving stock (Eq, Show, Enum, Bounded)

      unoTopology :: Topology UnoVertex
      unoTopology = Topology [(Initial, [Started])]
      |]
 )

deriving via AllVertices UnoVertex instance RenderableVertices UnoVertex

-- * State

data StateData = StateData
  { topCard :: Card
  , currentPlayer :: Player
  }

data UnoState (vertex :: UnoVertex) where
  UnoInitialState :: UnoState 'Initial
  UnoStartedState :: StateData -> UnoState 'Started

-- * Errors

data GameError
  = TooFewPlayers
  | GameAlreadyStarted
  | GameNotStarted
  deriving stock (Eq, Show)

-- * Machine

-- | A decider with the logic of the Uno game
--
-- It emits one event for every transition, not a list of events, because an
-- event represents a state transition and a state machine perform one single
-- state transition at every step
unoDecider
  :: InitialState UnoState
  -> Decider UnoTopology Command (Either GameError Event)
unoDecider initialState =
  Decider
    { deciderInitialState = initialState
    , decide = \command state ->
        case (state, command) of
          (_, StartGame initialData)
            | players initialData < PlayerCount 2 ->
                Left TooFewPlayers
          (UnoStartedState _, StartGame _) ->
            Left GameAlreadyStarted
          (_, StartGame initialData) ->
            Right $
              GameStarted
                initialData
                ( id $
                    nextPlayer
                      (firstCard initialData)
                      (init (PlayerId 0) $ players initialData)
                )
          (UnoInitialState, PlayCard _) ->
            Left GameNotStarted
          (UnoStartedState stateData, PlayCard playData)
            | id (currentPlayer stateData) /= playerId playData ->
                if sameColour (topCard stateData) (card playData) && sameValue (topCard stateData) (card playData)
                  then Right $ CardPlayed playData
                  else Right $ PlayerPlayedAtWrongTurn playData
          (UnoStartedState stateData, PlayCard playData) ->
            if sameColour (topCard stateData) (card playData) || sameValue (topCard stateData) (card playData)
              then
                Right $
                  CardPlayedAndTurnBegan
                    playData
                    ( id $
                        nextPlayer
                          (card playData)
                          (currentPlayer stateData)
                    )
              else Right $ WrongCardPlayed playData
    , evolve = \state eitherErrorEvent ->
        case eitherErrorEvent of
          Left _ -> EvolutionResult state
          Right event -> case (state, event) of
            (UnoInitialState, GameStarted initialData playerId) ->
              initialResult initialData playerId
            (UnoStartedState _, GameStarted initialData playerId) ->
              initialResult initialData playerId
            (UnoStartedState stateData, CardPlayed playData) ->
              playResult stateData playData Nothing
            (UnoStartedState stateData, CardPlayedAndTurnBegan playData playerId) ->
              playResult stateData playData (Just playerId)
            _ -> EvolutionResult state
    }
  where
    initialResult
      :: AllowedTransition UnoTopology initialVertex 'Started
      => InitialData
      -> PlayerId
      -> EvolutionResult UnoTopology UnoState initialVertex (Either GameError Event)
    initialResult initialData playerId =
      EvolutionResult $
        UnoStartedState $
          StateData
            { topCard = firstCard initialData
            , currentPlayer = init playerId $ players initialData
            }

    playResult
      :: StateData
      -> PlayData
      -> Maybe PlayerId
      -> EvolutionResult UnoTopology UnoState 'Started (Either GameError Event)
    playResult stateData playData maybePlayerId =
      EvolutionResult $
        UnoStartedState $
          StateData
            { topCard = card playData
            , currentPlayer = maybe Prelude.id set maybePlayerId $ currentPlayer stateData
            }