packages feed

poker-base-0.1.0.0: src/Poker/Game.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Representation of a game of holdem, including table structure, positioning, pot and betting state.
module Poker.Game
  ( Position (..),
    NumPlayers (..),
    numPlayersToWord8,
    numPlayersFromWord8,
    mkNumPlayers,
    allPositions,
    positionToTxt,
    getPreflopOrder,
    buttonPosition,
    bigBlindPosition,
    getPostFlopOrder,
    sortPostflop,
    Seat (..),
    Pot (..),
    Stack (..),
    Stake (..),
  )
where

import Data.Data
import Data.Text (Text)
import Data.Word (Word8)
import Poker.Cards
import Prettyprinter

-- | A player's 'Position' in a game of poker.
--
-- 'Position's are ordered by table order (clockwise). The smallest 'Position', @Position 0@,
-- is the first player to act preflop. The largest 'Position' is always the big blind.
--
-- >>> allPositions SixPlayers
-- [Position 0,Position 1,Position 2,Position 3,Position 4,Position 5]
-- >>> positionToTxt SixPlayers <$> allPositions SixPlayers
-- ["LJ","HJ","CO","BU","SB","BB"]
-- >>> positionToTxt NinePlayers <$> allPositions NinePlayers
-- ["UTG","UTG1","UTG2","LJ","HJ","CO","BU","SB","BB"]
--
-- The API for 'Position' is unstable. We are open to better ideas :)
newtype Position = Position Word8
  deriving (Read, Show, Enum, Bounded, Eq, Ord, Data, Typeable)

instance Pretty Position where
  pretty = viaShow

-- | Number of active players at a poker table. Players sitting out do not count, as
-- they do not contribute to the number of 'Position's.
data NumPlayers
  = TwoPlayers
  | ThreePlayers
  | FourPlayers
  | FivePlayers
  | SixPlayers
  | SevenPlayers
  | EightPlayers
  | NinePlayers
  deriving (Enum, Eq, Ord)

-- | Convert a 'NumPlayers' to a 'Word8'.
numPlayersToWord8 :: NumPlayers -> Word8
numPlayersToWord8 TwoPlayers = 2
numPlayersToWord8 ThreePlayers = 3
numPlayersToWord8 FourPlayers = 4
numPlayersToWord8 FivePlayers = 5
numPlayersToWord8 SixPlayers = 6
numPlayersToWord8 SevenPlayers = 7
numPlayersToWord8 EightPlayers = 8
numPlayersToWord8 NinePlayers = 9

-- | Convert a 'Word8' to a 'NumPlayers'.
numPlayersFromWord8 :: Word8 -> Maybe NumPlayers
numPlayersFromWord8 2 = Just TwoPlayers
numPlayersFromWord8 3 = Just ThreePlayers
numPlayersFromWord8 4 = Just FourPlayers
numPlayersFromWord8 5 = Just FivePlayers
numPlayersFromWord8 6 = Just SixPlayers
numPlayersFromWord8 7 = Just SevenPlayers
numPlayersFromWord8 8 = Just EightPlayers
numPlayersFromWord8 9 = Just NinePlayers
numPlayersFromWord8 _ = Nothing

-- | WARNING: The incoming 'Integral' is downcast to a 'Word8'
mkNumPlayers :: Integral a => a -> Maybe NumPlayers
mkNumPlayers num | num >= 2 && num <= 9 = numPlayersFromWord8 $ fromIntegral num
mkNumPlayers _ = Nothing

-- | >>> allPositions SixPlayers
-- [Position 0,Position 1,Position 2,Position 3,Position 4,Position 5]
allPositions :: NumPlayers -> [Position]
allPositions (numPlayersToWord8 -> num) = Position <$> [0 .. num - 1]

-- | >>> positionToTxt TwoPlayers <$> allPositions TwoPlayers
-- ["BU","BB"]
-- >>> positionToTxt SixPlayers <$> allPositions SixPlayers
-- ["LJ","HJ","CO","BU","SB","BB"]
-- >>> positionToTxt NinePlayers <$> allPositions NinePlayers
-- ["UTG","UTG1","UTG2","LJ","HJ","CO","BU","SB","BB"]
positionToTxt :: NumPlayers -> Position -> Text
positionToTxt (numPlayersToWord8 -> num) (Position pos) =
  let allPositionTexts = ["UTG", "UTG1", "UTG2", "LJ", "HJ", "CO", "BU", "SB", "BB"]
      positionTexts = case num of
        2 -> ["BU", "BB"]
        num' | num' > 2 && num' <= 9 -> drop (9 - fromIntegral num') allPositionTexts
        _ -> error $ "Unexpected NumPlayers value: " <> show num
   in positionTexts !! fromIntegral pos

-- | >>> positionToTxt TwoPlayers <$> getPreflopOrder TwoPlayers
-- ["BU","BB"]
-- >>> positionToTxt SixPlayers <$> getPreflopOrder SixPlayers
-- ["LJ","HJ","CO","BU","SB","BB"]
-- >>> positionToTxt NinePlayers <$> getPreflopOrder NinePlayers
-- ["UTG","UTG1","UTG2","LJ","HJ","CO","BU","SB","BB"]
getPreflopOrder :: NumPlayers -> [Position]
getPreflopOrder = allPositions

-- | >>> buttonPosition TwoPlayers
-- Position 0
-- >>> (\numPlayers -> positionToTxt numPlayers $ buttonPosition numPlayers) <$> enumFromTo TwoPlayers NinePlayers
-- ["BU","BU","BU","BU","BU","BU","BU","BU"]
buttonPosition :: NumPlayers -> Position
buttonPosition (numPlayersToWord8 -> num) = case num of
  2 -> Position 0
  _ -> Position (num - 3)

-- | >>> bigBlindPosition TwoPlayers
-- Position 1
-- >>> (\numPlayers -> positionToTxt numPlayers $ bigBlindPosition numPlayers) <$> enumFromTo TwoPlayers NinePlayers
-- ["BB","BB","BB","BB","BB","BB","BB","BB"]
bigBlindPosition :: NumPlayers -> Position
bigBlindPosition (numPlayersToWord8 -> num) = Position (num - 1)

-- | >>> positionToTxt TwoPlayers <$> getPostFlopOrder TwoPlayers
-- ["BB","BU"]
-- >>> positionToTxt ThreePlayers <$> getPostFlopOrder ThreePlayers
-- ["SB","BB","BU"]
-- >>> positionToTxt SixPlayers <$> getPostFlopOrder SixPlayers
-- ["SB","BB","LJ","HJ","CO","BU"]
-- >>> positionToTxt NinePlayers <$> getPostFlopOrder NinePlayers
-- ["SB","BB","UTG","UTG1","UTG2","LJ","HJ","CO","BU"]
getPostFlopOrder :: NumPlayers -> [Position]
getPostFlopOrder numPlayers@(fromIntegral . numPlayersToWord8 -> num) =
  take num
    . drop 1
    . dropWhile (/= buttonPosition numPlayers)
    . cycle
    $ allPositions numPlayers

-- | Sort a list of positions acccording to postflop ordering
--
-- >>> positionToTxt TwoPlayers <$> sortPostflop TwoPlayers (allPositions TwoPlayers)
-- ["BB","BU"]
-- >>> positionToTxt ThreePlayers <$> sortPostflop ThreePlayers (allPositions ThreePlayers)
-- ["SB","BB","BU"]
-- >>> positionToTxt SixPlayers <$> sortPostflop SixPlayers (allPositions SixPlayers)
-- ["SB","BB","LJ","HJ","CO","BU"]
-- >>> positionToTxt NinePlayers <$> sortPostflop NinePlayers (allPositions NinePlayers)
-- ["SB","BB","UTG","UTG1","UTG2","LJ","HJ","CO","BU"]
sortPostflop :: NumPlayers -> [Position] -> [Position]
sortPostflop num ps = filter (`elem` ps) $ getPostFlopOrder num

-- | Is a player hero or villain. Hero in poker means that the hand is from
-- the hero player's perspective.
data IsHero = Hero | Villain
  deriving (Read, Show, Eq, Ord, Enum, Bounded)

-- | A player's seat number at a poker table.
newtype Seat = Seat {_seat :: Int} deriving (Read, Show, Eq, Ord, Num)

-- | Total amount of money in the 'Pot'.
newtype Pot b = Pot {_pot :: b}
  deriving (Show, Eq, Ord, Num, Functor, Pretty, Semigroup, Monoid)

-- | Amount of money in a player's stack (not having been bet).
newtype Stack b = Stack {_stack :: b}
  deriving (Show, Eq, Ord, Num, Functor, Pretty, Semigroup)

-- | The state of a game with respect to cards turned and betting rounds.
data Board where
  RiverBoard :: !Card -> !Board -> Board
  TurnBoard :: !Card -> !Board -> Board
  FlopBoard :: (Card, Card, Card) -> !Board -> Board
  PreFlopBoard :: !Board -> Board
  InitialTable ::
    -- | Round where post actions occur.
    Board
  deriving (Eq, Ord, Show)

-- | Amount of money needed to join a game.
newtype Stake b = Stake {_stake :: b}
  deriving (Read, Show, Eq, Functor, Ord, Pretty)

-- | A bet done a player pre- or post-flop.
--
-- WARNING: Unstable API
data BetAction t
  = Call !t
  | Raise
      { raiseBy :: !t, -- TODO remove?
        raiseTo :: !t
      }
  | -- TODO remove AllInRaise
    AllInRaise
      { amountRaisedAI :: !t, -- TODO remove?
        raisedAITo :: !t
      }
  | Bet !t
  | -- TODO remove AllIn
    AllIn !t
  | Fold
  | Check
  deriving (Read, Show, Eq, Ord, Functor, Data, Typeable)