riichi-scoring 0.2.0.2 → 0.3.0.0
raw patch · 7 files changed
+890/−242 lines, 7 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Riichi.Meld: instance GHC.Internal.Show.Show Riichi.Meld.Meld
- Riichi.Meld: instance GHC.Internal.Show.Show Riichi.Meld.Pair
- Riichi.Meld: isClosed :: Meld -> Bool
- Riichi.Tile: instance GHC.Internal.Read.Read Riichi.Tile.Tile
- Riichi.Tile: instance GHC.Internal.Show.Show Riichi.Tile.Dragon
- Riichi.Tile: instance GHC.Internal.Show.Show Riichi.Tile.Honour
- Riichi.Tile: instance GHC.Internal.Show.Show Riichi.Tile.Suit
- Riichi.Tile: instance GHC.Internal.Show.Show Riichi.Tile.Tile
- Riichi.Tile: instance GHC.Internal.Show.Show Riichi.Tile.Wind
- Riichi.Yaku: yakuhai :: InterpretedHand -> Int
+ Riichi.Context: Context :: Maybe InterpretedHand -> HandContext -> Either YakuContext YakumanContext -> Context
+ Riichi.Context: HandContext :: Bool -> Bool -> RiichiContext -> WaitContext -> WindContext -> Bool -> Bool -> Integer -> HandContext
+ Riichi.Context: RiichiContext :: Bool -> Bool -> RiichiContext
+ Riichi.Context: WaitContext :: Bool -> Bool -> WaitContext
+ Riichi.Context: WindContext :: Wind -> Wind -> WindContext
+ Riichi.Context: YakuContext :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> HandContext -> Bool -> YakuContext
+ Riichi.Context: YakumanContext :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> YakumanContext
+ Riichi.Context: [dora] :: HandContext -> Integer
+ Riichi.Context: [isChanta] :: YakuContext -> Bool
+ Riichi.Context: [isChiitoitsu] :: YakuContext -> Bool
+ Riichi.Context: [isChinitsu] :: YakuContext -> Bool
+ Riichi.Context: [isChinroutou] :: YakumanContext -> Bool
+ Riichi.Context: [isChun] :: YakuContext -> Bool
+ Riichi.Context: [isChuurenPoutou] :: YakumanContext -> Bool
+ Riichi.Context: [isClosed] :: HandContext -> Bool
+ Riichi.Context: [isDaisangen] :: YakumanContext -> Bool
+ Riichi.Context: [isDaisuushii] :: YakumanContext -> Bool
+ Riichi.Context: [isHaku] :: YakuContext -> Bool
+ Riichi.Context: [isHatsu] :: YakuContext -> Bool
+ Riichi.Context: [isHonitsu] :: YakuContext -> Bool
+ Riichi.Context: [isHonroutou] :: YakuContext -> Bool
+ Riichi.Context: [isIipeikou] :: YakuContext -> Bool
+ Riichi.Context: [isIppatsu] :: RiichiContext -> Bool
+ Riichi.Context: [isIttsuu] :: YakuContext -> Bool
+ Riichi.Context: [isJunchan] :: YakuContext -> Bool
+ Riichi.Context: [isKokushiMusou] :: YakumanContext -> Bool
+ Riichi.Context: [isMenzenTsumo] :: YakuContext -> Bool
+ Riichi.Context: [isPinfu] :: YakuContext -> Bool
+ Riichi.Context: [isRiichi] :: RiichiContext -> Bool
+ Riichi.Context: [isRoundWind] :: YakuContext -> Bool
+ Riichi.Context: [isRyanmanWait] :: WaitContext -> Bool
+ Riichi.Context: [isRyanpeikou] :: YakuContext -> Bool
+ Riichi.Context: [isRyuuiisou] :: YakumanContext -> Bool
+ Riichi.Context: [isSanankou] :: YakuContext -> Bool
+ Riichi.Context: [isSankantsu] :: YakuContext -> Bool
+ Riichi.Context: [isSanshokuDoujun] :: YakuContext -> Bool
+ Riichi.Context: [isSanshokuDoukou] :: YakuContext -> Bool
+ Riichi.Context: [isSeatWind] :: YakuContext -> Bool
+ Riichi.Context: [isSevenPairs] :: HandContext -> Bool
+ Riichi.Context: [isShanponWait] :: WaitContext -> Bool
+ Riichi.Context: [isShousangen] :: YakuContext -> Bool
+ Riichi.Context: [isShousuushii] :: YakumanContext -> Bool
+ Riichi.Context: [isSuuankou] :: YakumanContext -> Bool
+ Riichi.Context: [isSuukantsu] :: YakumanContext -> Bool
+ Riichi.Context: [isTanyao] :: YakuContext -> Bool
+ Riichi.Context: [isThirteenOrphans] :: HandContext -> Bool
+ Riichi.Context: [isToitoi] :: YakuContext -> Bool
+ Riichi.Context: [isTsumo] :: HandContext -> Bool
+ Riichi.Context: [isTsuuiisou] :: YakumanContext -> Bool
+ Riichi.Context: [riichi] :: HandContext -> RiichiContext
+ Riichi.Context: [roundWind] :: WindContext -> Wind
+ Riichi.Context: [seatWind] :: WindContext -> Wind
+ Riichi.Context: [wait] :: HandContext -> WaitContext
+ Riichi.Context: [wind] :: HandContext -> WindContext
+ Riichi.Context: [yakuHandContext] :: YakuContext -> HandContext
+ Riichi.Context: addClosedContext :: Maybe InterpretedHand -> HandContext -> IO (Maybe InterpretedHand, HandContext)
+ Riichi.Context: addRiichiContext :: HandContext -> IO HandContext
+ Riichi.Context: addTsumoContext :: HandContext -> IO HandContext
+ Riichi.Context: addWaitContext :: HandContext -> IO HandContext
+ Riichi.Context: addWindContext :: HandContext -> IO HandContext
+ Riichi.Context: askRiichiContext :: IO RiichiContext
+ Riichi.Context: askWaitContext :: IO WaitContext
+ Riichi.Context: askWindContext :: IO WindContext
+ Riichi.Context: askYesNo :: String -> IO Bool
+ Riichi.Context: closeHandContext :: HandContext -> HandContext
+ Riichi.Context: data Context
+ Riichi.Context: data HandContext
+ Riichi.Context: data RiichiContext
+ Riichi.Context: data WaitContext
+ Riichi.Context: data WindContext
+ Riichi.Context: data YakuContext
+ Riichi.Context: data YakumanContext
+ Riichi.Context: getMinimalHandContext :: Hand -> Bool -> HandContext
+ Riichi.Context: mkContext :: Hand -> IO Context
+ Riichi.Context: mkYakuContext :: Hand -> Maybe InterpretedHand -> HandContext -> YakuContext
+ Riichi.Context: mkYakumanContext :: Hand -> Maybe InterpretedHand -> Maybe Bool -> IO (Maybe YakumanContext)
+ Riichi.Context: openHandContext :: HandContext -> HandContext
+ Riichi.Meld: getOpenMelds :: [Meld] -> IO [Meld]
+ Riichi.Meld: getRonMeld :: [Meld] -> IO [Meld]
+ Riichi.Meld: instance GHC.Show.Show Riichi.Meld.Meld
+ Riichi.Meld: instance GHC.Show.Show Riichi.Meld.Pair
+ Riichi.Meld: meldIsClosed :: Meld -> Bool
+ Riichi.Scoring: _getFu :: InterpretedHand -> HandContext -> Fu
+ Riichi.Scoring: formContextString :: Context -> String
+ Riichi.Scoring: formYakuString :: YakuContext -> String
+ Riichi.Scoring: formYakumanString :: YakumanContext -> String
+ Riichi.Scoring: getContextFu :: Context -> Fu
+ Riichi.Scoring: getContextHanOrYakumans :: Context -> Either Han Int
+ Riichi.Scoring: getContextHansOrYakumans :: Context -> Either (Han, Han) Int
+ Riichi.Scoring: getYakuHan :: YakuContext -> Han
+ Riichi.Scoring: getYakumanCount :: YakumanContext -> Int
+ Riichi.Tile: instance GHC.Read.Read Riichi.Tile.Tile
+ Riichi.Tile: instance GHC.Show.Show Riichi.Tile.Dragon
+ Riichi.Tile: instance GHC.Show.Show Riichi.Tile.Honour
+ Riichi.Tile: instance GHC.Show.Show Riichi.Tile.Suit
+ Riichi.Tile: instance GHC.Show.Show Riichi.Tile.Tile
+ Riichi.Tile: instance GHC.Show.Show Riichi.Tile.Wind
+ Riichi.Yaku: chiitoitsu :: Hand -> Bool
+ Riichi.Yaku: yakuhaiDragons :: InterpretedHand -> Int
Files
- CHANGELOG.md +12/−0
- riichi-scoring.cabal +2/−1
- src/Riichi/Context.hs +363/−0
- src/Riichi/Display.hs +260/−177
- src/Riichi/Meld.hs +23/−2
- src/Riichi/Scoring.hs +150/−6
- src/Riichi/Yaku.hs +80/−56
CHANGELOG.md view
@@ -18,3 +18,15 @@ ## 0.2.0.2 Internal library now unnamed. Hoping this fixes Hackage build fail.++## 0.2.0.3+Added docs for yaku functions.+Reworked seven pairs detection to ensure hand has 14 tiles.+9 gates now checks that the hand is closed.++## 0.3.0.0+Fixed error in scoring triple triplets, was previously scored as 1 han open.+Implemented new backend for scoring and yaku detection via "context" based+approach, defined in new Context submodule. This helps simplify function+signatures for operations that need many pieces of information besides just the+superficial composition of a hand.
riichi-scoring.cabal view
@@ -20,7 +20,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.2.0.2+version: 0.3.0.0 -- A short (one-line) description of the package. synopsis: A CLI tool for interpreting and scoring Riichi Mahjong hands. -- A longer description of the package.@@ -54,6 +54,7 @@ library exposed-modules:+ Riichi.Context Riichi.Display Riichi.Meld Riichi.Scoring
+ src/Riichi/Context.hs view
@@ -0,0 +1,363 @@+module Riichi.Context where++import Data.Function ((&))+import Riichi.Meld+import Riichi.Tile+import Riichi.Yaku++askYesNo :: String -> IO Bool+askYesNo string = do+ putStrLn string+ input <- getLine+ if input == "y" then return True else return False++{- | Record to track the additional context for a hand. Fields with a Maybe type are information that+we may not need in order to fully understand the hand.+-}+data HandContext = HandContext+ { isClosed :: Bool+ , isTsumo :: Bool+ , riichi :: RiichiContext+ , wait :: WaitContext+ , wind :: WindContext+ , isSevenPairs :: Bool+ , isThirteenOrphans :: Bool+ , dora :: Integer+ }++getMinimalHandContext :: Hand -> Bool -> HandContext+getMinimalHandContext hand sevenPairs =+ let+ orphans = thirteenOrphans hand+ in+ HandContext+ { isClosed = True+ , isTsumo = False+ , riichi = RiichiContext{isRiichi = False, isIppatsu = False}+ , wait = WaitContext{isRyanmanWait = False, isShanponWait = False}+ , wind = WindContext{seatWind = East, roundWind = East}+ , isSevenPairs = sevenPairs+ , isThirteenOrphans = orphans+ , dora = hand & map getDora & sum+ }++openHandContext :: HandContext -> HandContext+openHandContext handContext = handContext{isClosed = False}++closeHandContext :: HandContext -> HandContext+closeHandContext handContext = handContext{isClosed = True}++addRiichiContext :: HandContext -> IO HandContext+addRiichiContext handContext = do+ riichiContext <- askRiichiContext+ return handContext{riichi = riichiContext}++addWaitContext :: HandContext -> IO HandContext+addWaitContext handContext = do+ waitContext <- askWaitContext+ return handContext{wait = waitContext}++addWindContext :: HandContext -> IO HandContext+addWindContext handContext = do+ windContext <- askWindContext+ return handContext{wind = windContext}++addTsumoContext :: HandContext -> IO HandContext+addTsumoContext handContext = do+ tsumo <- askYesNo "Tsumo? [y/n]:"+ return handContext{isTsumo = tsumo}++-- The context must already know riichi and tsumo values for this to work+addClosedContext :: Maybe InterpretedHand -> HandContext -> IO (Maybe InterpretedHand, HandContext)+addClosedContext (Just (tile, melds)) handContext = do+ let HandContext{isTsumo = t, riichi = RiichiContext{isRiichi = r}} = handContext+ melds' <- case (r, t) of+ (True, True) -> return melds+ (True, False) -> getRonMeld melds+ (False, _) -> getOpenMelds melds+ let numOpen = melds' & filter isOpen & length+ closedHand <- case numOpen of+ 0 -> return True+ _+ | numOpen > 1 -> return False+ | otherwise -> case (r, t) of+ (True, _) -> return True+ (False, True) -> return False -- Already know there is an open meld. Now we know it wasn't opened by Ron.+ (False, False) -> askYesNo "Damaten? [y/n]:"+ return (Just (tile, melds'), handContext{isClosed = closedHand})+addClosedContext Nothing handContext = return (Nothing, handContext{isClosed = True})++data WaitContext = WaitContext+ { isRyanmanWait :: Bool+ , isShanponWait :: Bool+ }++askWaitContext :: IO (WaitContext)+askWaitContext = do+ ryanmanWait <- askYesNo "Did the hand have an open wait? [y/n]: "+ shanponWait <-+ if ryanmanWait+ then return False+ else askYesNo "Did the hand have a dual pair wait? [y/n]: "+ return WaitContext{isRyanmanWait = ryanmanWait, isShanponWait = shanponWait}++data WindContext = WindContext+ {seatWind :: Wind, roundWind :: Wind}++askWindContext :: IO (WindContext)+askWindContext = do+ putStrLn "Input round and seat wind: "+ (Honour (Wind r) _) : (Honour (Wind s) _) : _ <- mkHand <$> getLine+ return WindContext{seatWind = s, roundWind = r}++data RiichiContext = RiichiContext+ {isRiichi :: Bool, isIppatsu :: Bool}++askRiichiContext :: IO (RiichiContext)+askRiichiContext = do+ riichi <- askYesNo "Riichi? [y/n]: "+ ippatsu <-+ if riichi+ then askYesNo "Ippatsu? [y/n]: "+ else return False+ return RiichiContext{isRiichi = riichi, isIppatsu = ippatsu}++data YakuContext = YakuContext+ -- , isTsumo :: Bool+ { isPinfu :: Bool+ , isTanyao :: Bool+ , isHaku :: Bool+ , isHatsu :: Bool+ , isChun :: Bool+ , isSeatWind :: Bool+ , isRoundWind :: Bool+ , isSanshokuDoujun :: Bool+ , isSanshokuDoukou :: Bool+ , isSanankou :: Bool+ , isToitoi :: Bool+ , isIttsuu :: Bool+ , isSankantsu :: Bool+ , isShousangen :: Bool+ , isChinitsu :: Bool+ , isHonitsu :: Bool+ , isRyanpeikou :: Bool+ , isIipeikou :: Bool+ , isJunchan :: Bool+ , isChanta :: Bool+ , isHonroutou :: Bool+ , isMenzenTsumo :: Bool+ , yakuHandContext :: HandContext+ , -- , isThirteenOrphans :: Bool+ isChiitoitsu :: Bool+ }++mkYakuContext :: Hand -> Maybe InterpretedHand -> HandContext -> YakuContext+mkYakuContext hand (Just ih) handContext =+ let+ HandContext+ { wind = WindContext{seatWind = sw, roundWind = rw}+ , wait = WaitContext{isRyanmanWait = isRyanman}+ , isClosed = closure+ , isTsumo = tsumo+ } = handContext+ (fullFlush, halfFlush) = (chinitsu hand, honitsu hand)+ (twicePure, singlePure) = (ryanpeikou ih, iipeikou ih)+ (fullyOutside, halfOutside, terminalsHonours) = (junchan ih, chanta ih, honroutou hand)+ in+ YakuContext+ { isPinfu = pinfu ih sw rw isRyanman closure+ , isTanyao = tanyao hand+ , isHaku = haku ih+ , isHatsu = hatsu ih+ , isChun = chun ih+ , isSeatWind = checkWind sw ih+ , isRoundWind = checkWind rw ih+ , isSanshokuDoujun = sanshokuDoujun ih+ , isSanshokuDoukou = sanshokuDoukou ih+ , isSanankou = sanankou ih+ , isToitoi = toitoi ih+ , isIttsuu = ittsuu ih+ , isSankantsu = sankantsu ih+ , isShousangen = shousangen ih+ , isChinitsu = fullFlush+ , isHonitsu = halfFlush && (not fullFlush)+ , -- Should these check for closed, or do we want to include them anyway?+ isRyanpeikou = twicePure && closure+ , isIipeikou = singlePure && (not twicePure) && closure+ , isJunchan = fullyOutside+ , isChanta = halfOutside && (not fullyOutside) && (not terminalsHonours)+ , isHonroutou = terminalsHonours && (not fullyOutside)+ , yakuHandContext = handContext+ , isMenzenTsumo = tsumo && closure+ , isChiitoitsu = False+ }+-- Seven pairs case+mkYakuContext hand Nothing handContext@HandContext{isTsumo = tsumo} =+ let+ (fullFlush, halfFlush) = (chinitsu hand, honitsu hand)+ terminalsHonours = honroutou hand+ in+ YakuContext+ { isPinfu = False+ , isTanyao = tanyao hand+ , isHaku = False+ , isHatsu = False+ , isChun = False+ , isSeatWind = False+ , isRoundWind = False+ , isSanshokuDoujun = False+ , isSanshokuDoukou = False+ , isSanankou = False+ , isToitoi = False+ , isIttsuu = False+ , isSankantsu = False+ , isShousangen = False+ , isChinitsu = fullFlush+ , isHonitsu = halfFlush && (not fullFlush)+ , -- Should these check for closed, or do we want to include them anyway?+ isRyanpeikou = False+ , isIipeikou = False+ , isJunchan = False+ , isChanta = False+ , isHonroutou = terminalsHonours+ , yakuHandContext = handContext+ , isMenzenTsumo = tsumo+ , isChiitoitsu = chiitoitsu hand+ }++data YakumanContext = YakumanContext+ { isSuuankou :: Bool+ , isSuukantsu :: Bool+ , isDaisangen :: Bool+ , isShousuushii :: Bool+ , isTsuuiisou :: Bool+ , isChinroutou :: Bool+ , isRyuuiisou :: Bool+ , isChuurenPoutou :: Bool+ , isDaisuushii :: Bool+ , isKokushiMusou :: Bool+ }++-- maybeClosure var allows us to shortcicuit the IO check. If we already know whether the hand is closed or not,+-- we don't ask. This way, the yaku command doesn't ask, and the score command does.+mkYakumanContext :: Hand -> Maybe InterpretedHand -> Maybe Bool -> IO (Maybe YakumanContext)+mkYakumanContext hand (Just ih) maybeClosure =+ do+ let isSuuaa = suuankou ih+ let isSuuka = suukantsu ih+ let isDaisa = daisangen ih+ let isShous = shousuushii ih+ let isTsuui = tsuuiisou hand+ let isChinr = chinroutou hand+ let isRyuui = ryuuiisou hand+ isChuur <-+ if chuurenPoutou hand+ then case maybeClosure of+ Nothing -> askYesNo "Is the hand closed? [y/n]: "+ Just True -> return True+ Just False -> return False+ else return False+ let isDaisu = daisuushii ih+ if or [isSuuaa, isSuuka, isDaisa, isShous, isTsuui, isChinr, isRyuui, isChuur, isDaisu]+ then+ return $+ Just+ YakumanContext+ { isSuuankou = isSuuaa+ , isSuukantsu = isSuuka+ , isDaisangen = isDaisa+ , isShousuushii = isShous+ , isTsuuiisou = isTsuui+ , isChinroutou = isChinr+ , isRyuuiisou = isRyuui+ , isChuurenPoutou = isChuur+ , isDaisuushii = isDaisu+ , isKokushiMusou = False+ }+ else+ return $ Nothing+mkYakumanContext hand Nothing _ =+ let+ isTsuui = tsuuiisou hand+ isChinr = chinroutou hand+ isRyuui = ryuuiisou hand+ in+ if (or [isTsuui, isChinr, isRyuui]) && chiitoitsu hand+ then+ return $+ Just+ YakumanContext+ { isSuuankou = False+ , isSuukantsu = False+ , isDaisangen = False+ , isShousuushii = False+ , isTsuuiisou = isTsuui+ , isChinroutou = isChinr+ , isRyuuiisou = isRyuui+ , isChuurenPoutou = False+ , isDaisuushii = False+ , isKokushiMusou = False+ }+ else+ if thirteenOrphans hand+ then+ return $+ Just+ YakumanContext+ { isSuuankou = False+ , isSuukantsu = False+ , isDaisangen = False+ , isShousuushii = False+ , isTsuuiisou = False+ , isChinroutou = False+ , isRyuuiisou = False+ , isChuurenPoutou = False+ , isDaisuushii = False+ , isKokushiMusou = True+ }+ else return Nothing+data Context = Context (Maybe InterpretedHand) HandContext (Either YakuContext YakumanContext)++mkContext :: Hand -> IO Context+mkContext hand = do+ sevenPairs <-+ if chiitoitsu hand+ then askYesNo "Seven pairs? [y/n]: "+ else return False+ let handContext@HandContext{isThirteenOrphans = orphans} = getMinimalHandContext hand sevenPairs+ maybeIh <-+ if sevenPairs || orphans+ then+ return Nothing+ else do+ let ihs = interpretHand hand+ if length ihs == 0+ then undefined+ else do+ ih <-+ if length ihs > 1+ then do+ putStrLn "Select hand interpretation: "+ sequence_ $ [("[" ++ show n ++ "]: " ++ (ih & showInterpretedHand)) & putStrLn | (n :: Integer, ih) <- zip [0 ..] ihs]+ n <- read <$> getLine :: IO Int+ putStrLn ""+ return (ihs !! n)+ else do+ putStrLn "Found one way to interpret this hand: "+ let ih = head ihs+ putStrLn (showInterpretedHand ih)+ putStrLn ""+ return ih+ return $ Just ih+ maybeYakumanContext <- mkYakumanContext hand maybeIh Nothing+ case maybeYakumanContext of+ Nothing -> do+ handContext' <-+ if not sevenPairs+ then pure handContext >>= addWindContext >>= addRiichiContext >>= addTsumoContext >>= addWaitContext+ else pure handContext >>= addRiichiContext >>= addTsumoContext+ (maybeIh', handContext'') <- addClosedContext maybeIh handContext'+ let yakuContext = mkYakuContext hand maybeIh' handContext''+ return $ Context maybeIh' handContext'' (Left yakuContext)+ Just yakumanContext -> do+ return $ Context maybeIh handContext (Right yakumanContext)
src/Riichi/Display.hs view
@@ -7,9 +7,12 @@ module Riichi.Display where import ColourStrings+import Control.Monad (forM)+import Control.Monad.Trans import Data.Function import Data.List (intersperse, sort) import Data.Monoid (getSum)+import Riichi.Context import Riichi.Meld import Riichi.Scoring import Riichi.Tile@@ -30,26 +33,51 @@ if num >= 1 then do if num == 1- then putStrLn $ "Found " ++ show num ++ " way to interpret this hand:\n"+ then putStrLn $ "Found 1 way to interpret this hand:\n" else putStrLn $ "Found " ++ show num ++ " ways to interpret this hand:\n"- putStrLn $ concat $ do- ih <- ihs- let hand_string = ih & showInterpretedHand- let (value, yaku_string) = getYaku hand (Just ih) False False False False East East False- return $ case value of- Left (hanClosed, hanOpen) -> hand_string ++ "\n" ++ yaku_string ++ "\t\t" ++ toGreen (show (getSum hanClosed)) ++ " Han total if closed, " ++ toGreen (show (getSum hanOpen)) ++ " if open\n"- Right yakumans -> hand_string ++ "\n" ++ yaku_string ++ "\t\t" ++ toGreen (show (getSum yakumans)) ++ " Yakuman total\n"+ _ <- forM ihs $ \ih -> do+ let handString = showInterpretedHand ih+ let handContext = getMinimalHandContext hand False+ maybeYakumanContext <- mkYakumanContext hand (Just ih) (Just True)+ let context = case maybeYakumanContext of+ Nothing ->+ let yakuContext = mkYakuContext hand (Just ih) handContext+ in (Context (Just ih) handContext (Left yakuContext))+ Just yakumanContext -> (Context (Just ih) handContext (Right yakumanContext))+ let string = formContextString context+ let hanOrYakumans = getContextHansOrYakumans context+ case hanOrYakumans of+ Left (hanClosed, hanOpen) ->+ putStrLn $+ handString+ ++ "\n"+ ++ string+ ++ "\t\t"+ ++ toGreen (show (getSum hanClosed))+ ++ " Han total if closed, "+ ++ toGreen (show (getSum hanOpen))+ ++ " if open\n"+ Right yakumans -> putStrLn $ handString ++ "\n" ++ string ++ "\t\t" ++ toGreen (show yakumans) ++ " Yakuman total\n"+ return () else return ()- if allPairs hand || thirteenOrphans hand+ if (chiitoitsu hand) || (thirteenOrphans hand) then do if num == 0 then putStrLn "This hand can be interpreted as:\n" else putStrLn "This hand can also be interpreted as:\n"- let hand_string = hand & sort & map show & intersperse ", " & concat- let (value, yaku_string) = getYaku hand Nothing False False False False East East False- putStrLn $ case value of- Left (han, _) -> hand_string ++ "\n" ++ yaku_string ++ "\t\t" ++ toGreen (show (getSum han)) ++ " Han total, closed by definition\n"- Right yakumans -> hand_string ++ "\n" ++ yaku_string ++ "\t\t" ++ toGreen (show (getSum yakumans)) ++ " Yakuman total\n"+ let handString = hand & sort & map show & intersperse ", " & concat+ let handContext = getMinimalHandContext hand True+ maybeYakumanContext <- mkYakumanContext hand Nothing (Just True)+ let context = case maybeYakumanContext of+ Nothing ->+ let yakuContext = mkYakuContext hand Nothing handContext+ in (Context Nothing handContext (Left yakuContext))+ Just yakumanContext -> (Context Nothing handContext (Right yakumanContext))+ let string = formContextString context+ let hanOrYakumans = getContextHanOrYakumans context+ putStrLn $ case hanOrYakumans of+ Left han -> handString ++ "\n" ++ string ++ "\t\t" ++ toGreen (show (getSum han)) ++ " Han total, closed by definition\n"+ Right yakumans -> handString ++ "\n" ++ string ++ "\t\t" ++ toGreen (show yakumans) ++ " Yakuman total\n" else if num == 0 then putStrLn $ toRed "This hand is not valid"@@ -64,170 +92,225 @@ -- | Implements the "score" command for the CLI. In need of a refactor, logic is rather serpentine at the moment. displayHandScore :: Hand -> IO () displayHandScore hand = do- putStrLn "Input dora: (or leave blank)"- dora <- mkHand <$> getLine- let hand' = addDora dora hand- putStrLn "Input round and seat wind: "- (Honour (Wind roundWind) _) : (Honour (Wind seatWind) _) : _ <- mkHand <$> getLine- putStrLn "Riichi? [y/n]: "- riichi <- (== "y") <$> getLine- ippatsu <-- if riichi- then do- putStrLn "Ippatsu? [y/n]: "- (== "y") <$> getLine- else return False- putStrLn "Tsumo? [y/n]: "- input <- getLine- let tsumo = (input == "y")- sevenPairs <-- if (allPairs hand') && (length hand' == 14)- then do- putStrLn "Seven pairs? [y/n]: "- input <- getLine- if input == "y"- then do- let (value, yaku_string) = getYaku hand' Nothing riichi ippatsu tsumo False seatWind roundWind True- putStrLn $ case value of- Left (han, _) ->- yaku_string- ++ "\t\t"- ++ toGreen (show (getSum han))- ++ " Han total, closed by definition\n"- ++ "\n\t"- ++ toGreen (show (getScore han 25 True tsumo))- ++ " points for Dealer, "- ++ toGreen (show (getScore han 25 False tsumo))- ++ " points for Non-Dealer."- ++ ( if name /= ""- then- " ("- ++ toMagenta name- ++ ")."- else ""- )- where- name = hanToHandName han- Right yakumans ->- yaku_string- ++ "\t\t"- ++ toGreen (show (getSum yakumans))- ++ " Yakuman total\n"- ++ "\n\t"- ++ toGreen (show (getSum yakumans * 48000))- ++ " points for Dealer, "- ++ toGreen (show (getSum yakumans * 32000))- ++ " points for Non-Dealer."- return True- else- return False- else return False- if sevenPairs == False- then do- let ihs = interpretHand hand'- maybeIh <-- if thirteenOrphans hand'- then return Nothing- else do- (pair, melds) <-- if length ihs > 1- then do- putStrLn "Select hand interpretation: "- sequence_ $ [("[" ++ show n ++ "]: " ++ (ih & showInterpretedHand)) & putStrLn | (n :: Integer, ih) <- zip [0 ..] ihs]- n <- read <$> getLine :: IO Int- return (ihs !! n)- else do- putStrLn "Found one way to interpret this hand: "- let ih = head ihs- putStrLn (showInterpretedHand ih)- return ih-- melds' <- case (riichi, tsumo) of- (True, True) -> return melds- (True, False) -> do- putStrLn "Which meld was opened by Ron? (enter an index): "- sequence_ $ [("[" ++ show i ++ "]: " ++ (meld & show)) & putStrLn | (i :: Integer, meld) <- zip [0 ..] melds]- input <- getLine- let index :: Int = input & read- return $ (zip [0 ..] melds) & map (\(i, meld) -> if i == index then openMeld meld else meld)- (False, _) -> do- putStrLn "Which melds are open? (enter a string of indices, or leave blank if all closed): "- sequence_ $ [("[" ++ show i ++ "]: " ++ (meld & show)) & putStrLn | (i :: Integer, meld) <- zip [0 ..] melds]- input <- getLine- let indices :: [Int] = input & map return & (map read)- return $ (zip [0 ..] melds) & map (\(i, meld) -> if i `elem` indices then openMeld meld else meld)- return $ Just (pair, melds')-- putStrLn "Did the hand have an open wait? [y/n]: "- ryanmanWait <- (== "y") <$> getLine- shanponWait <-- if ryanmanWait- then return False- else do- putStrLn "Did the hand have a dual pair wait? [y/n]: "- (== "y") <$> getLine- let goodWait = not (ryanmanWait || shanponWait)+ if length hand < 14+ then putStrLn "Hand is the wrong size"+ else do+ putStrLn "Input dora (or leave blank):"+ dora <- mkHand <$> getLine+ if dora /= []+ then+ putStrLn ""+ else return () - closedHand <- case maybeIh of- Just (_, melds) ->- case numOpen of- 0 -> return True- _- | numOpen > 1 -> return False- | otherwise -> case (riichi, tsumo) of- (True, _) -> return True- (False, True) -> return False -- Already know there is an open meld. Now we know it wasn't opened by Ron.- (False, False) ->- ( do- putStrLn "Damaten? [y/n]:"- (== "y") <$> getLine- )- where- numOpen = melds & filter isOpen & length- Nothing -> return $ True- let (value, yaku_string) = getYaku hand' maybeIh riichi ippatsu tsumo ryanmanWait seatWind roundWind closedHand- let fu = case maybeIh of- Just ih ->- if pinfu ih seatWind roundWind ryanmanWait closedHand- then if tsumo then 20 else 30- else getFu ih seatWind roundWind goodWait tsumo closedHand- -- Seven pairs already taken care of, so Nothing signifies thirteen orphans or an invalid hand.- -- So yakuman or invalid - 0 Fu, we will say.- Nothing -> 0- putStrLn $ case value of- Left (hanClosed, hanOpen) ->- "\tYaku:\n"- ++ yaku_string- ++ "\t\t"- ++ openClosed- ++ toGreen (show (getSum han))- ++ " Han total, with "- ++ toBlue (show fu)- ++ " Fu\n"- ++ "\n\t"- ++ toGreen (show (getScore han fu True tsumo))- ++ " points for Dealer, "- ++ toGreen (show (getScore han fu False tsumo))- ++ " points for Non-Dealer"- ++ ( if name /= ""- then- " ("- ++ toMagenta name- ++ ")."- else ""- )+ let hand' = addDora dora hand+ context@(Context _ handContext _) <- mkContext hand'+ let string = formContextString context+ let tsumo = isTsumo handContext+ let closure = isClosed handContext+ let hanOrYakumans = getContextHanOrYakumans context+ case hanOrYakumans of+ Left han -> do+ let fu = getContextFu context+ let name = hanToHandName han+ putStrLn $+ "\tYaku:\n"+ ++ string+ ++ "\t\t"+ ++ openClosed+ ++ toGreen (show (getSum han))+ ++ " Han total, with "+ ++ toBlue (show fu)+ ++ " Fu\n"+ ++ "\n\t"+ ++ toGreen (show (getScore han fu True tsumo))+ ++ " points for Dealer, "+ ++ toGreen (show (getScore han fu False tsumo))+ ++ " points for Non-Dealer"+ ++ ( if name /= ""+ then+ " ("+ ++ toMagenta name+ ++ ")."+ else ""+ ) where- han = if (closedHand) then hanClosed else hanOpen name = hanToHandName han- openClosed = if closedHand then "Closed hand: " else "Open hand: "+ openClosed = if closure then "Closed hand: " else "Open hand: " Right yakumans ->- yaku_string- ++ "\t\t"- ++ toGreen (show (getSum yakumans))- ++ " Yakuman total\n"- ++ "\n\t"- ++ toGreen (show (getSum yakumans * 48000))- ++ " points for Dealer, "- ++ toGreen (show (getSum yakumans * 32000))- ++ " points for Non-Dealer."- else return ()+ putStrLn $+ string+ ++ "\t\t"+ ++ toGreen (show (yakumans))+ ++ " Yakuman total\n"+ ++ "\n\t"+ ++ toGreen (show (yakumans * 48000))+ ++ " points for Dealer, "+ ++ toGreen (show (yakumans * 32000))+ ++ " points for Non-Dealer."++-- putStrLn "Input round and seat wind: "+-- (Honour (Wind roundWind) _) : (Honour (Wind seatWind) _) : _ <- mkHand <$> getLine+-- putStrLn "Riichi? [y/n]: "+-- riichi <- (== "y") <$> getLine+-- ippatsu <-+-- if riichi+-- then do+-- putStrLn "Ippatsu? [y/n]: "+-- (== "y") <$> getLine+-- else return False+-- putStrLn "Tsumo? [y/n]: "+-- input <- getLine+-- let tsumo = (input == "y")+--+-- sevenPairs <-+-- if chiitoitsu hand'+-- then do+-- putStrLn "Seven pairs? [y/n]: "+-- input <- getLine+-- if input == "y"+-- then+-- return True+-- else+-- return False+-- else return False++-- if sevenPairs == True+-- then do+-- let (value, yaku_string) = getYaku hand' Nothing riichi ippatsu tsumo False seatWind roundWind True+-- putStrLn $ case value of+-- Left (han, _) ->+-- yaku_string+-- ++ "\t\t"+-- ++ toGreen (show (getSum han))+-- ++ " Han total, closed by definition\n"+-- ++ "\n\t"+-- ++ toGreen (show (getScore han 25 True tsumo))+-- ++ " points for Dealer, "+-- ++ toGreen (show (getScore han 25 False tsumo))+-- ++ " points for Non-Dealer."+-- ++ ( if name /= ""+-- then+-- " ("+-- ++ toMagenta name+-- ++ ")."+-- else ""+-- )+-- where+-- name = hanToHandName han+-- Right yakumans ->+-- yaku_string+-- ++ "\t\t"+-- ++ toGreen (show (getSum yakumans))+-- ++ " Yakuman total\n"+-- ++ "\n\t"+-- ++ toGreen (show (getSum yakumans * 48000))+-- ++ " points for Dealer, "+-- ++ toGreen (show (getSum yakumans * 32000))+-- ++ " points for Non-Dealer."+-- else do+-- let ihs = interpretHand hand'+-- maybeIh <-+-- if thirteenOrphans hand'+-- then return Nothing+-- else do+-- (pair, melds) <-+-- if length ihs > 1+-- then do+-- putStrLn "Select hand interpretation: "+-- sequence_ $ [("[" ++ show n ++ "]: " ++ (ih & showInterpretedHand)) & putStrLn | (n :: Integer, ih) <- zip [0 ..] ihs]+-- n <- read <$> getLine :: IO Int+-- return (ihs !! n)+-- else do+-- putStrLn "Found one way to interpret this hand: "+-- let ih = head ihs+-- putStrLn (showInterpretedHand ih)+-- return ih+--+-- melds' <- case (riichi, tsumo) of+-- (True, True) -> return melds+-- (True, False) -> getRonMeld melds+-- -- putStrLn "Which meld was opened by Ron? (enter an index): "+-- -- sequence_ $ [("[" ++ show i ++ "]: " ++ (meld & show)) & putStrLn | (i :: Integer, meld) <- zip [0 ..] melds]+-- -- input <- getLine+-- -- let index :: Int = input & read+-- -- return $ (zip [0 ..] melds) & map (\(i, meld) -> if i == index then openMeld meld else meld+-- (False, _) -> getOpenMelds melds+-- -- putStrLn "Which melds are open? (enter a string of indices, or leave blank if all closed): "+-- -- sequence_ $ [("[" ++ show i ++ "]: " ++ (meld & show)) & putStrLn | (i :: Integer, meld) <- zip [0 ..] melds]+-- -- input <- getLine+-- -- let indices :: [Int] = input & map return & (map read)+-- -- return $ (zip [0 ..] melds) & map (\(i, meld) -> if i `elem` indices then openMeld meld else meld)+-- return $ Just (pair, melds')+--+-- putStrLn "Did the hand have an open wait? [y/n]: "+-- ryanmanWait <- (== "y") <$> getLine+-- shanponWait <-+-- if ryanmanWait+-- then return False+-- else do+-- putStrLn "Did the hand have a dual pair wait? [y/n]: "+-- (== "y") <$> getLine+-- let goodWait = not (ryanmanWait || shanponWait)+--+-- closedHand <- case maybeIh of+-- Just (_, melds) ->+-- case numOpen of+-- 0 -> return True+-- _+-- | numOpen > 1 -> return False+-- | otherwise -> case (riichi, tsumo) of+-- (True, _) -> return True+-- (False, True) -> return False -- Already know there is an open meld. Now we know it wasn't opened by Ron.+-- (False, False) ->+-- ( do+-- putStrLn "Damaten? [y/n]:"+-- (== "y") <$> getLine+-- )+-- where+-- numOpen = melds & filter isOpen & length+-- Nothing -> return $ True+-- let (value, yaku_string) = getYaku hand' maybeIh riichi ippatsu tsumo ryanmanWait seatWind roundWind closedHand+-- let fu = case maybeIh of+-- Just ih ->+-- if pinfu ih seatWind roundWind ryanmanWait closedHand+-- then if tsumo then 20 else 30+-- else getFu ih seatWind roundWind goodWait tsumo closedHand+-- -- Seven pairs already taken care of, so Nothing signifies thirteen orphans or an invalid hand.+-- -- So yakuman or invalid - 0 Fu, we will say.+-- Nothing -> 0+-- putStrLn $ case value of+-- Left (hanClosed, hanOpen) ->+-- "\tYaku:\n"+-- ++ yaku_string+-- ++ "\t\t"+-- ++ openClosed+-- ++ toGreen (show (getSum han))+-- ++ " Han total, with "+-- ++ toBlue (show fu)+-- ++ " Fu\n"+-- ++ "\n\t"+-- ++ toGreen (show (getScore han fu True tsumo))+-- ++ " points for Dealer, "+-- ++ toGreen (show (getScore han fu False tsumo))+-- ++ " points for Non-Dealer"+-- ++ ( if name /= ""+-- then+-- " ("+-- ++ toMagenta name+-- ++ ")."+-- else ""+-- )+-- where+-- han = if (closedHand) then hanClosed else hanOpen+-- name = hanToHandName han+-- openClosed = if closedHand then "Closed hand: " else "Open hand: "+-- Right yakumans ->+-- yaku_string+-- ++ "\t\t"+-- ++ toGreen (show (getSum yakumans))+-- ++ " Yakuman total\n"+-- ++ "\n\t"+-- ++ toGreen (show (getSum yakumans * 48000))+-- ++ " points for Dealer, "+-- ++ toGreen (show (getSum yakumans * 32000))+-- ++ " points for Non-Dealer."
src/Riichi/Meld.hs view
@@ -39,6 +39,7 @@ data Meld = Chi Tile Tile Tile Open | Pon Tile Open | Kan Tile Open deriving (Ord) type Open = Bool +-- | Almost identical to what deriving Eq would generate, except we consider open and closed melds that are otherwise equal to be the same. instance Eq Meld where (==) (Pon tile1 _) (Pon tile2 _) = tile1 == tile2 (==) (Kan tile1 _) (Kan tile2 _) = tile1 == tile2@@ -85,8 +86,8 @@ isOpen (Pon _ x) = x isOpen (Kan _ x) = x -isClosed :: Meld -> Bool-isClosed = not . isOpen+meldIsClosed :: Meld -> Bool+meldIsClosed = not . isOpen openMeld :: Meld -> Meld openMeld (Chi a b c _) = Chi a b c True@@ -170,6 +171,7 @@ concatMelds (Kan tile _ : rest) = [tile, tile, tile, tile] ++ concatMelds rest concatMelds (Chi tile1 tile2 tile3 _ : rest) = [tile1, tile2, tile3] ++ concatMelds rest +-- | Find the unique pairs in a hand. Returns a list of pairs, each along with the remaining tiles in the hand not in the pair. findPairs :: Hand -> [(Pair, Hand)] findPairs hand = hand@@ -223,3 +225,22 @@ showInterpretedHand :: InterpretedHand -> String showInterpretedHand (pair, melds) = (show pair) : (map show melds) & intersperse ", " & concat++getRonMeld :: [Meld] -> IO [Meld]+getRonMeld melds = do+ putStrLn "Which meld was opened by Ron? (leave blank if it was the pair): "+ sequence_ $ [("[" ++ show i ++ "]: " ++ (meld & show)) & putStrLn | (i :: Integer, meld) <- zip [0 ..] melds]+ input <- getLine+ if input == ""+ then return melds+ else+ let index :: Int = input & read+ in return $ (zip [0 ..] melds) & map (\(i, meld) -> if i == index then openMeld meld else meld)++getOpenMelds :: [Meld] -> IO [Meld]+getOpenMelds melds = do+ putStrLn "Which melds are open? (enter a string of indices, or leave blank if all closed): "+ sequence_ $ [("[" ++ show i ++ "]: " ++ (meld & show)) & putStrLn | (i :: Integer, meld) <- zip [0 ..] melds]+ input <- getLine+ let indices :: [Int] = input & map return & (map read)+ return $ (zip [0 ..] melds) & map (\(i, meld) -> if i `elem` indices then openMeld meld else meld)
src/Riichi/Scoring.hs view
@@ -11,7 +11,8 @@ import Control.Monad.Writer import Data.Function ((&)) import Data.Map qualified as M-import Data.Monoid (Sum)+import Data.Monoid (Sum (..))+import Riichi.Context import Riichi.Meld import Riichi.Tile import Riichi.Yaku@@ -33,14 +34,14 @@ when (tsuuiisou hand) $ tell (1, toMagenta "\tYakuman: All Honours\n") when (chinroutou hand) $ tell (1, toMagenta "\tYakuman: All Terminals\n") when (ryuuiisou hand) $ tell (1, toMagenta "\tYakuman: All Green\n")- when (chuurenPoutou hand) $ tell (1, toMagenta "\tYakuman: Nine Gates\n")+ when (chuurenPoutou hand && closedHand) $ tell (1, toMagenta "\tYakuman: Nine Gates\n") when (daisuushii ih) $ tell (2, toMagenta "\tDouble Yakuman: Big Winds\n") (_, (yakumans, yakumanOutput)) = runWriter yakumanWriter hanWriter :: Writer (Han, Han, String) () = do when (riichi) $ tell (1, 0, toCyan "\t1 Han: Riichi\n") when (ippatsu) $ tell (1, 0, toCyan "\t1 Han: Ippatsu\n")- when (tsumo && and (map isClosed melds)) $ tell (1, 0, toCyan "\t1 Han: Fully concealed hand\n")+ when (tsumo && and (map meldIsClosed melds)) $ tell (1, 0, toCyan "\t1 Han: Fully concealed hand\n") when (pinfu ih seatWind roundWind ryanmanWait closedHand) $ tell (1, 0, toCyan "\t1 Han: Pinfu\n") when (tanyao hand) $ tell (1, 1, toCyan "\t1 Han: All simples\n") when (haku ih) $ tell (1, 1, toCyan "\t1 Han: Haku (White Dragon)\n")@@ -49,7 +50,7 @@ when (checkWind seatWind ih) $ tell (1, 1, toCyan "\t1 Han: Seat wind\n") when (checkWind roundWind ih) $ tell (1, 1, toCyan "\t1 Han: Round wind\n") when (sanshokuDoujun ih) $ tell (2, 1, toCyan "\t2 Han: Mixed triple sequence (-1 Han if open)\n")- when (sanshokuDoukou ih) $ tell (2, 1, toCyan "\t2 Han: Triple triplets (-1 Han if open)\n")+ when (sanshokuDoukou ih) $ tell (2, 2, toCyan "\t2 Han: Triple triplets\n") when (sanankou ih) $ tell (2, 2, toCyan "\t2 Han: Three concealed triplets\n") let (fullFlush, halfFlush) = (chinitsu hand, honitsu hand) if fullFlush@@ -111,7 +112,6 @@ when (tsuuiisou hand) $ tell (1, toMagenta "\tYakuman: All Honours (+ seven pairs)\n") (_, (yakumans, yakumanOutput)) = runWriter yakumanWriter - sevenPairs = allPairs hand hanWriter :: Writer (Han, String) () = do when (riichi) $ tell (1, toCyan "\t1 Han: Riichi\n") when (ippatsu) $ tell (1, toCyan "\t1 Han: Ippatsu\n")@@ -137,7 +137,7 @@ if yakumans > 0 then (Right yakumans, yakumanOutput) else- if sevenPairs+ if chiitoitsu hand then (Left (han + 2, 0), toCyan "\t2 Han: Seven pairs\n" ++ output) else@@ -203,6 +203,150 @@ else case M.lookup (han, fu) scoreTableRonNonDealer of Just score -> score Nothing -> 0++formYakuString :: YakuContext -> String+formYakuString yakuContext@YakuContext{yakuHandContext = handContext@HandContext{riichi = riichiContext, dora}} =+ let hanWriter :: Writer String () = do+ when (isRiichi riichiContext) $ tell $ toCyan "\t1 Han: Riichi\n"+ when (isIppatsu riichiContext) $ tell $ toCyan "\t1 Han: Ippatsu\n"+ when (isMenzenTsumo yakuContext) $ tell $ toCyan "\t1 Han: Fully concealed hand\n"+ when (isChiitoitsu yakuContext) $ tell $ toCyan "\t2 Han: Seven pairs\n"+ when (isPinfu yakuContext) $ tell $ toCyan "\t1 Han: Pinfu\n"+ when (isTanyao yakuContext) $ tell $ toCyan "\t1 Han: All simples\n"+ when (isHaku yakuContext) $ tell $ toCyan "\t1 Han: Haku (White Dragon)\n"+ when (isHatsu yakuContext) $ tell $ toCyan "\t1 Han: Hatsu (Green Dragon)\n"+ when (isChun yakuContext) $ tell $ toCyan "\t1 Han: Chun (Red Dragon)\n"+ when (isSeatWind yakuContext) $ tell $ toCyan "\t1 Han: Seat wind\n"+ when (isRoundWind yakuContext) $ tell $ toCyan "\t1 Han: Round wind\n"+ when (isSanshokuDoujun yakuContext) $ tell $ toCyan "\t2 Han: Mixed triple sequence (-1 Han if open)\n"+ when (isSanshokuDoukou yakuContext) $ tell $ toCyan "\t2 Han: Triple triplets\n"+ when (isSanankou yakuContext) $ tell $ toCyan "\t2 Han: Three concealed triplets\n"+ when (isChinitsu yakuContext) $ tell $ toCyan "\t6 Han: Full flush (-1 Han if open)\n"+ when (isHonitsu yakuContext) $ tell $ toCyan "\t3 Han: Half flush (-1 Han if open)\n"+ when (isToitoi yakuContext) $ tell $ toCyan "\t2 Han: All triplets\n"+ when (isIttsuu yakuContext) $ tell $ toCyan "\t2 Han: Pure straight (-1 Han if open)\n"+ when (isSankantsu yakuContext) $ tell $ toCyan "\t2 Han: Three kans\n"+ when (isShousangen yakuContext) $ tell $ toCyan "\t2 Han: Little three dragons\n"+ when (isRyanpeikou yakuContext) $ tell $ toCyan "\t3 Han: Twice pure double sequence (Closed only)\n"+ when (isIipeikou yakuContext) $ tell $ toCyan "\t1 Han: Pure double sequence (Closed only)\n"+ when (isJunchan yakuContext) $ tell $ toCyan "\t3 Han: Fully outside hand (-1 Han if open)\n"+ when (isHonroutou yakuContext) $ tell $ toCyan "\t2 Han: All terminals and honours\n"+ when (isChanta yakuContext) $ tell $ toCyan "\t2 Han: Half outside hand (-1 Han if open)\n"+ when (dora > 0) $ tell $ toCyan ("\t" ++ show dora ++ " Han: Dora\n")+ (_, string) = runWriter hanWriter+ in string++getYakuHan :: YakuContext -> Han+getYakuHan yakuContext@YakuContext{yakuHandContext = handContext@HandContext{riichi = riichiContext, dora}} =+ let+ closedBonus = if (isClosed handContext == True) then 1 else 0+ hanWriter :: Writer Han () = do+ when (isRiichi riichiContext) $ tell $ 1+ when (isIppatsu riichiContext) $ tell $ 1+ when (isMenzenTsumo yakuContext) $ tell $ 1+ when (isChiitoitsu yakuContext) $ tell $ 2+ when (isPinfu yakuContext) $ tell $ 1+ when (isTanyao yakuContext) $ tell $ 1+ when (isHaku yakuContext) $ tell $ 1+ when (isHatsu yakuContext) $ tell $ 1+ when (isChun yakuContext) $ tell $ 1+ when (isSeatWind yakuContext) $ tell $ 1+ when (isRoundWind yakuContext) $ tell $ 1+ when (isSanshokuDoujun yakuContext) $ tell $ 1 + closedBonus+ when (isSanshokuDoukou yakuContext) $ tell $ 2+ when (isSanankou yakuContext) $ tell $ 2+ when (isChinitsu yakuContext) $ tell $ 5 + closedBonus+ when (isHonitsu yakuContext) $ tell $ 2 + closedBonus+ when (isToitoi yakuContext) $ tell $ 2+ when (isIttsuu yakuContext) $ tell $ 1 + closedBonus+ when (isSankantsu yakuContext) $ tell $ 2+ when (isShousangen yakuContext) $ tell $ 2+ when (isRyanpeikou yakuContext) $ tell $ 3 * closedBonus+ when (isIipeikou yakuContext) $ tell $ closedBonus+ when (isJunchan yakuContext) $ tell $ 2 + closedBonus+ when (isHonroutou yakuContext) $ tell $ 2+ when (isChanta yakuContext) $ tell $ 1 + closedBonus+ tell $ fromInteger dora+ (_, han) = runWriter hanWriter+ in+ han++formYakumanString :: YakumanContext -> String+formYakumanString yakumanContext =+ let+ yakumanWriter :: Writer String () = do+ when (isSuuankou yakumanContext) $ tell $ toMagenta "\tYakuman: Four Concealed Triplets\n"+ when (isSuukantsu yakumanContext) $ tell $ toMagenta "\tYakuman: Four Kans\n"+ when (isDaisangen yakumanContext) $ tell $ toMagenta "\tYakuman: Big Four Dragons\n"+ when (isShousuushii yakumanContext) $ tell $ toMagenta "\tYakuman: Little Winds\n"+ when (isTsuuiisou yakumanContext) $ tell $ toMagenta "\tYakuman: All Honours\n"+ when (isChinroutou yakumanContext) $ tell $ toMagenta "\tYakuman: All Terminals\n"+ when (isRyuuiisou yakumanContext) $ tell $ toMagenta "\tYakuman: All Green\n"+ when (isChuurenPoutou yakumanContext) $ tell $ toMagenta "\tYakuman: Nine Gates\n"+ when (isDaisuushii yakumanContext) $ tell $ toMagenta "\tDouble Yakuman: Big Winds\n"+ when (isKokushiMusou yakumanContext) $ tell $ toMagenta "\tYakuman: Thirteen Orphans\n"+ (_, string) = runWriter yakumanWriter+ in+ string+getYakumanCount :: YakumanContext -> Int+getYakumanCount yakumanContext =+ let+ yakumanWriter :: Writer (Sum Int) () = do+ when (isSuuankou yakumanContext) $ tell 1+ when (isSuukantsu yakumanContext) $ tell 1+ when (isDaisangen yakumanContext) $ tell 1+ when (isShousuushii yakumanContext) $ tell 1+ when (isTsuuiisou yakumanContext) $ tell 1+ when (isChinroutou yakumanContext) $ tell 1+ when (isRyuuiisou yakumanContext) $ tell 1+ when (isChuurenPoutou yakumanContext) $ tell 1+ when (isDaisuushii yakumanContext) $ tell 2+ when (isKokushiMusou yakumanContext) $ tell 1+ (_, yakumans) = runWriter yakumanWriter+ in+ getSum yakumans++formContextString :: Context -> String+formContextString (Context _ _ (Left yakuContext)) = formYakuString yakuContext+formContextString (Context _ _ (Right yakumanContext)) = formYakumanString yakumanContext++getContextHanOrYakumans :: Context -> Either Han Int+getContextHanOrYakumans (Context _ _ (Left yakuContext)) = Left $ getYakuHan yakuContext+getContextHanOrYakumans (Context _ _ (Right yakumanContext)) = Right $ getYakumanCount yakumanContext++getContextHansOrYakumans :: Context -> Either (Han, Han) Int+getContextHansOrYakumans (Context _ _ (Left yakuContext@YakuContext{yakuHandContext = handContext})) =+ Left $+ ( getYakuHan yakuContext{yakuHandContext = closeHandContext handContext}+ , getYakuHan yakuContext{yakuHandContext = openHandContext handContext}+ )+getContextHansOrYakumans (Context _ _ (Right yakumanContext)) = Right $ getYakumanCount yakumanContext++_getFu :: InterpretedHand -> HandContext -> Fu+_getFu (Pair tile, melds) c =+ let+ sw = seatWind $ wind c+ rw = roundWind $ wind c+ goodWait = not $ (isRyanmanWait $ wait c) || (isShanponWait $ wait c)+ tsumo = isTsumo c+ closure = isClosed c+ meldsFu = melds & map getMeldFu & sum+ waitFu = if goodWait then 2 else 0+ yakuhaiFu =+ (if (tile & isDragon) then 2 else 0)+ + (if (tile == (Honour (Wind rw) 0)) then 2 else 0)+ + (if (tile == (Honour (Wind sw) 0)) then 2 else 0)+ ronClosedFu = if (not tsumo) && closure then 10 else 0+ tsumoFu = if tsumo then 2 else 0+ in+ roundUp (20 + meldsFu + waitFu + yakuhaiFu + ronClosedFu + tsumoFu)+ where+ roundUp n = last ([120, 110 .. 10] & filter (>= n))++-- Partial function!+getContextFu :: Context -> Fu+getContextFu (Context (Just ih) handContext _) = _getFu ih handContext+getContextFu (Context Nothing handContext _) = 25 scoreTableTsumoDealer :: M.Map (Han, Fu) Integer scoreTableTsumoDealer =
src/Riichi/Yaku.hs view
@@ -6,7 +6,7 @@ -} module Riichi.Yaku where -import Data.Either (lefts, rights)+import Data.Either (isLeft, lefts, rights) import Data.Function import Data.List import Data.Set qualified as Set@@ -19,37 +19,45 @@ -- Some of these could probably be rephrased to be point free but I think -- that would just make them more confusing. +-- | Check a hand for tanyao, aka all simples tanyao :: Hand -> Bool tanyao hand = hand & (map isSimple) & and +{- | Check if a hand is composed entirely of unique pairs.+Note this doesn't check the hand has the right size to be seven pairs.+-} allPairs :: Hand -> Bool allPairs hand = (hand & findPairs & length) * 2 == length hand --- Yakuman+-- | Check a hand for the yaku seven pairs+chiitoitsu :: Hand -> Bool+chiitoitsu hand = (allPairs hand) && (length hand == 14)++-- | Check if a hand is thirteen orphans, a yakuman. thirteenOrphans :: Hand -> Bool thirteenOrphans hand =- ( Set.fromList hand- == Set.fromList- [ (Honour $ Dragon $ Red) 0- , (Honour $ Dragon $ White) 0- , (Honour $ Dragon $ Green) 0- , (Honour $ Wind $ North) 0- , (Honour $ Wind $ South) 0- , (Honour $ Wind $ East) 0- , (Honour $ Wind $ West) 0- , (Numeric Pin 1) 0- , (Numeric Pin 9) 0- , (Numeric Man 1) 0- , (Numeric Man 9) 0- , (Numeric Sou 1) 0- , (Numeric Sou 9) 0- ]- )- && (length hand == 14)+ (length hand == 14)+ && ( Set.fromList hand+ == Set.fromList+ [ (Honour $ Dragon $ Red) 0+ , (Honour $ Dragon $ White) 0+ , (Honour $ Dragon $ Green) 0+ , (Honour $ Wind $ North) 0+ , (Honour $ Wind $ South) 0+ , (Honour $ Wind $ East) 0+ , (Honour $ Wind $ West) 0+ , (Numeric Pin 1) 0+ , (Numeric Pin 9) 0+ , (Numeric Man 1) 0+ , (Numeric Man 9) 0+ , (Numeric Sou 1) 0+ , (Numeric Sou 9) 0+ ]+ ) --- Counts the number of yakuhai pairs. One han each.-yakuhai :: InterpretedHand -> Int-yakuhai (_, melds) =+-- | Counts the number of yakuhai dragon triplets. One han each.+yakuhaiDragons :: InterpretedHand -> Int+yakuhaiDragons (_, melds) = melds & ( filter ( \meld ->@@ -62,37 +70,46 @@ ) & length +-- | Check for the presence of a triplet in an interpreted hand. checkPon :: Tile -> InterpretedHand -> Bool checkPon tile (_, melds) = Pon tile False `elem` melds +-- | Check for the presence of a white dragon triplet in an interpreted hand. haku :: InterpretedHand -> Bool haku = checkPon ((Honour $ Dragon $ White) 0) +-- | Check for the presence of a green dragon triplet in an interpreted hand. hatsu :: InterpretedHand -> Bool hatsu = checkPon ((Honour $ Dragon $ Green) 0) +-- | Check for the presence of a red dragon triplet in an interpreted hand. chun :: InterpretedHand -> Bool chun = checkPon ((Honour $ Dragon $ Red) 0) +-- | Check for the presence of a north wind triplet in an interpreted hand. checkNorth :: InterpretedHand -> Bool checkNorth = checkPon ((Honour $ Wind $ North) 0) +-- | Check for the presence of a east wind triplet in an interpreted hand. checkEast :: InterpretedHand -> Bool checkEast = checkPon ((Honour $ Wind $ East) 0) +-- | Check for the presence of a south wind triplet in an interpreted hand. checkSouth :: InterpretedHand -> Bool checkSouth = checkPon ((Honour $ Wind $ South) 0) +-- | Check for the presence of a west wind triplet in an interpreted hand. checkWest :: InterpretedHand -> Bool checkWest = checkPon ((Honour $ Wind $ West) 0) +-- | Check for the presence of a given wind triplet in an interpreted hand. checkWind :: Wind -> InterpretedHand -> Bool checkWind East = checkEast checkWind North = checkNorth checkWind West = checkWest checkWind South = checkSouth --- Same sequence in all three suits+-- | Check for the same sequence in all three suit sanshokuDoujun :: InterpretedHand -> Bool sanshokuDoujun (_, melds) = let@@ -108,7 +125,7 @@ | base <- [1 .. 7] ] --- Same triplet (or Kan!) in all three suits+-- | Check for the same triplet (or quad) in all three suits sanshokuDoukou :: InterpretedHand -> Bool sanshokuDoukou (_, melds) = let@@ -124,25 +141,20 @@ | base <- [1 .. 9] ] --- Full flush--- chinitsu :: InterpretedHand -> Bool--- chinitsu (pair, melds) = allEqual ((getPairSuit pair) : (melds & (map getMeldSuit)))+-- | Check if a hand is a full flush chinitsu :: Hand -> Bool-chinitsu hand = hand & map getTileSuit & allEqual+chinitsu hand = (hand & map getTileSuit & allEqual) && (hand & head & getTileSuit & isLeft) --- Half flush--- Only check equality on the lefts of Either Suit Honour, i.e the suited melds.--- honitsu :: InterpretedHand -> Bool--- honitsu (pair, melds) =--- ((getPairSuit pair) : (melds & (map getMeldSuit)))--- & lefts--- & allEqual+-- | Check if a hand is a half flush honitsu :: Hand -> Bool+-- Only check equality on the lefts of Either Suit Honour, i.e the suited melds. honitsu hand = hand & map getTileSuit & lefts & allEqual +-- | Check if a hand is all triplets toitoi :: InterpretedHand -> Bool toitoi (_, melds) = melds & (map (\meld -> meldIsPon meld || meldIsKan meld)) & and +-- | Check if a hand has 1-9 in a single suit ittsuu :: InterpretedHand -> Bool ittsuu (_, melds) = let@@ -158,17 +170,19 @@ | suit <- [Man, Pin, Sou] ] --- Three quads (open or closed)+-- | Check a hand for three quads (open or closed) sankantsu :: InterpretedHand -> Bool sankantsu (_, melds) = melds & (filter meldIsKan) & length & (3 ==) --- Four quads (open or closed). Yakuman+-- | Check a hand for four quads (open or closed). Yakuman suukantsu :: InterpretedHand -> Bool suukantsu (_, melds) = melds & (filter meldIsKan) & length & (4 ==) -- Little three dragons. Worth noting that we permit ourselves to assume that hands -- don't contain more than 4 of a given tile! So no need to worry about multiple melds -- of the same dragon.++-- | Check a hand for little three dragons shousangen :: InterpretedHand -> Bool shousangen (Pair tile, melds) = (isDragon tile)@@ -180,7 +194,7 @@ & (2 ==) ) --- Big three dragons. Yakuman+-- | Check a hand for big three dragons. Yakuman daisangen :: InterpretedHand -> Bool daisangen (_, melds) = ( melds@@ -191,7 +205,7 @@ & (3 ==) ) --- Little winds. Yakuman+-- | Check a hand for little winds. Yakuman shousuushii :: InterpretedHand -> Bool shousuushii (Pair tile, melds) = (isWind tile)@@ -203,7 +217,7 @@ & (3 ==) ) --- Big winds. Double Yakuman+-- | Check a hand for big winds. Double Yakuman daisuushii :: InterpretedHand -> Bool daisuushii (_, melds) = ( melds@@ -214,35 +228,41 @@ & (4 ==) ) --- Pure double sequence. Closed only!+-- | Check for pure double sequence. Closed only, but this function doesn't check that. iipeikou :: InterpretedHand -> Bool iipeikou (_, melds) = melds & filter meldIsChi & sort & group & map length & filter (< 4) & filter (>= 2) & length & (== 1) --- Twice pure double sequence. Note we require the two pairs of sequences to be distinct.+-- | Check for twice pure double sequence. Closed only, but this function doesn't check that. ryanpeikou :: InterpretedHand -> Bool ryanpeikou (_, melds) = melds & filter meldIsChi & sort & group & map length & filter (< 4) & filter (>= 2) & length & (>= 2) --- Half outside hand+-- | Check for half outside hand chanta :: InterpretedHand -> Bool chanta (Pair tile, melds) = (melds & map getMeldBase & lefts & filter (\x -> x /= 1 && x /= 7)) == [] && (not $ isSimple tile) --- Fully outside hand (chanta + no honours)+-- | Check for fully outside hand (chanta + no honnours) junchan :: InterpretedHand -> Bool junchan ih@(Pair tile, melds) = (isNumeric tile) && (melds & map getMeldBase & rights) == [] && (chanta ih) --- All terminals and honours+-- | Check for all terminals and honours honroutou :: Hand -> Bool honroutou hand = hand & map (\tile -> isHonour tile || isTerminal tile) & and --- All honours. Yakuman+{- | Check for all honours. Yakuman.+Does not check that the hand is valid to begin with.+-} tsuuiisou :: Hand -> Bool tsuuiisou hand = hand & map isHonour & and --- All terminals. Yakuman+{- | Check for all terminals. Yakuman.+Does not check that the hand is valid to begin with.+-} chinroutou :: Hand -> Bool chinroutou hand = hand & map isTerminal & and --- All green. Yakuman+{- | Check for all green. Yakuman.+Does not check that the hand is valid to begin with.+-} ryuuiisou :: Hand -> Bool ryuuiisou hand = hand & map isGreen & and where@@ -250,21 +270,25 @@ isGreen (Honour (Dragon Green) _) = True isGreen _ = False --- Nine Gates. Yakuman--- Length == 9 precludes the possibility of a all honours chinitsu.+{- | Check for nine gates. Yakuman.+Does not check that the hand is valid to begin with, nor that it is closed.+(Indeed, checking closure of the hand would require more information, thus complicating the function signature).+Note that kans are dissalowed by rule. A valid hand + length == 14 (which this function checks) ensures this.+-} chuurenPoutou :: Hand -> Bool-chuurenPoutou hand = (chinitsu hand) && (length list == 9) && (head list >= 3) && (last list >= 3)+chuurenPoutou hand = (chinitsu hand) && (length list == 9) && (head list >= 3) && (last list >= 3) && (length hand == 14) where list = (hand & sort & group & map length) --- Three concealed triplets+-- | Check for three concealed triplets. Note that a closed hand may have a triplet that is not concealed, if won by ron. sanankou :: InterpretedHand -> Bool-sanankou (_, melds) = (melds & filter (not . meldIsChi) & filter (isClosed) & length) == 3+sanankou (_, melds) = (melds & filter (not . meldIsChi) & filter (meldIsClosed) & length) == 3 --- Four concealed triplets+-- | Check for four concealed triplets. Yakuman. suuankou :: InterpretedHand -> Bool-suuankou (_, melds) = (melds & filter (not . meldIsChi) & filter (isClosed) & length) == 4+suuankou (_, melds) = (melds & filter (not . meldIsChi) & filter (meldIsClosed) & length) == 4 +-- | Check for pinfu. Takes the hand, the seat wind, round wind, whether the wait was ryanman, and whether it is closed (in that order). pinfu :: InterpretedHand -> Wind -> Wind -> Bool -> Bool -> Bool pinfu (Pair tile, melds) seatWind roundWind ryanmanWait closedHand = (melds & filter (not . meldIsChi)) == []