Haskell でポーカーの役判定処理 #2

Haskell でポーカーの役判定処理 - satosystemsの日記 の続き。

ポーカーの役判定処理をイチから書き直した。
改善されたのは以下。

  • エースが数値で最も強いルールに改善
  • 絵柄による役の強さに改善
  • ジョーカーがあっても判定できるように改善
  • いくつかのバグを修正
  • ロジックの改善


今ひとつよくわからない点は、コード内にコメントしてある。

ほとんどが Card というデータ型に関する悩み。具体的には Card は Card Suit Rank というコンストラクタと C2, C3, C4 というコンストラクタがあって、ロジックでは前者を、リテラルで書きたいときや入出力では後者を利用したいという要求から、Card データ型は Read, Show, Ord, Eq, Enum をすべて定義している。その実装が冗長なので何とかしたい。

module Poker (
  Suit(..),
  Rank(..),
  Card(..),
  Hand,
  HandRank(..),
  ranking,
) where

import Data.List (find, findIndices, sort)

-- 絵柄の弱い順に定義
-- J はジョーカーを表し、B は役判定処理でのブランクカードを表す
data Suit = J | B | C | D | H | S deriving (Show, Ord, Eq, Enum)

instance Read Suit where
  readsPrec _ s = readSuit s

readSuit :: ReadS Suit
readSuit ""       = []
readSuit ('J':cs) = [(J, cs)]
readSuit ('B':cs) = [(B, cs)]
readSuit ('C':cs) = [(C, cs)]
readSuit ('D':cs) = [(D, cs)]
readSuit ('H':cs) = [(H, cs)]
readSuit ('S':cs) = [(S, cs)]
readSuit _        = []

-- 数値の弱い順に定義
-- Zero はジョーカーとブランクカードでのみ利用される
data Rank = Zero | Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King | Ace deriving (Ord, Eq, Enum)

instance Read Rank where
  readsPrec _ s = readRank s

-- この部分をエレガントに書けないものか
readRank :: ReadS Rank
readRank ""           = []
readRank ('0':cs)     = [(Zero, cs)]
readRank ('2':cs)     = [(Two, cs)]
readRank ('3':cs)     = [(Three, cs)]
readRank ('4':cs)     = [(Four, cs)]
readRank ('5':cs)     = [(Five, cs)]
readRank ('6':cs)     = [(Six, cs)]
readRank ('7':cs)     = [(Seven, cs)]
readRank ('8':cs)     = [(Eight, cs)]
readRank ('9':cs)     = [(Nine, cs)]
readRank ('1':'0':cs) = [(Ten, cs)]
readRank ('1':'1':cs) = [(Jack, cs)]
readRank ('1':'2':cs) = [(Queen, cs)]
readRank ('1':'3':cs) = [(King, cs)]
readRank ('1':cs)     = [(Ace, cs)]
readRank _            = []

-- この部分をエレガントに書けないものか
instance Show Rank where
  show Zero  = "0"
  show Two   = "2"
  show Three = "3"
  show Four  = "4"
  show Five  = "5"
  show Six   = "6"
  show Seven = "7"
  show Eight = "8"
  show Nine  = "9"
  show Ten   = "10"
  show Jack  = "11"
  show Queen = "12"
  show King  = "13"
  show Ace   = "1"

data Card = Card { suit :: Suit, rank :: Rank }
  | J0 | B0
  | C2 | D2 | H2 | S2 | C3 | D3 | H3 | S3 | C4 | D4 | H4 | S4
  | C5 | D5 | H5 | S5 | C6 | D6 | H6 | S6 | C7 | D7 | H7 | S7
  | C8 | D8 | H8 | S8 | C9 | D9 | H9 | S9 | C10 | D10 | H10 | S10
  | C11 | D11 | H11 | S11 | C12 | D12 | H12 | S12
  | C13 | D13 | H13 | S13 | C1 | D1 | H1 | S1

-- Card のコンストラクタとほとんどかぶるこのリスト定義はなんとかならないか
labels = [ "J0", "B0"
         , "C2", "D2", "H2", "S2", "C3", "D3", "H3", "S3", "C4", "D4", "H4", "S4"
         , "C5", "D5", "H5", "S5", "C6", "D6", "H6", "S6", "C7", "D7", "H7", "S7"
         , "C8", "D8", "H8", "S8", "C9", "D9", "H9", "S9", "C10", "D10", "H10", "S10"
         , "C11", "D11", "H11", "S11", "C12", "D12", "H12", "S12"
         , "C13", "D13", "H13", "S13", "C1", "D1", "H1", "S1"
         ]

-- これも何とかならないものか
instance Show Card where
  show (Card su ra) = show su ++ show ra
  show J0           = labels !! 0
  show B0           = labels !! 1
  show C2           = labels !! 2
  show D2           = labels !! 3
  show H2           = labels !! 4
  show S2           = labels !! 5
  show C3           = labels !! 6
  show D3           = labels !! 7
  show H3           = labels !! 8
  show S3           = labels !! 9
  show C4           = labels !! 10
  show D4           = labels !! 11
  show H4           = labels !! 12
  show S4           = labels !! 13
  show C5           = labels !! 14
  show D5           = labels !! 15
  show H5           = labels !! 16
  show S5           = labels !! 17
  show C6           = labels !! 18
  show D6           = labels !! 19
  show H6           = labels !! 20
  show S6           = labels !! 21
  show C7           = labels !! 22
  show D7           = labels !! 23
  show H7           = labels !! 24
  show S7           = labels !! 25
  show C8           = labels !! 26
  show D8           = labels !! 27
  show H8           = labels !! 28
  show S8           = labels !! 29
  show C9           = labels !! 30
  show D9           = labels !! 31
  show H9           = labels !! 32
  show S9           = labels !! 33
  show C10          = labels !! 34
  show D10          = labels !! 35
  show H10          = labels !! 36
  show S10          = labels !! 37
  show C11          = labels !! 38
  show D11          = labels !! 39
  show H11          = labels !! 40
  show S11          = labels !! 41
  show C12          = labels !! 42
  show D12          = labels !! 43
  show H12          = labels !! 44
  show S12          = labels !! 45
  show C13          = labels !! 46
  show D13          = labels !! 47
  show H13          = labels !! 48
  show S13          = labels !! 49
  show C1           = labels !! 50
  show D1           = labels !! 51
  show H1           = labels !! 52
  show S1           = labels !! 53

instance Read Card where
  readsPrec _ s = readCard s

readCard :: ReadS Card
readCard "" = []
readCard s = case readSuit s of
  []          -> []
  [(su, cs1)] -> case readRank cs1 of
    []          -> []
    [(ra, cs2)] -> [(Card su ra, cs2)]

instance Ord Card where
  compare ca1 ca2 = compare (fromEnum ca1) (fromEnum ca2)

instance Enum Card where
  toEnum n = read $ labels !! n
  fromEnum ca = findIndices (show ca ==) labels !! 0

instance Eq Card where
  x == y = (fromEnum x) == (fromEnum y)

-- 配列にするとロジックが書きやすい
type Hand = [Card]

-- 役の弱い順に定義
data HandRank = HighCard Card
              | OnePair Rank Card
              | TwoPair Rank Rank Card
              | ThreeCard Rank
              | Straight Card
              | Flush Card
              | FullHouse Rank
              | FourCard Rank
              | StraightFlush Card
              | RoyalStraightFlush Suit
              | FiveCard Rank deriving (Show, Read, Eq, Ord)

wildcard :: Hand -> [Card]
wildcard ha = filter (\ca -> find (ca ==) ha == Nothing) [B0 .. S1]

ranking :: Hand -> HandRank
ranking = ranking' . sort . map (toEnum . fromEnum) -- 正規化してからロジックに渡す

-- ここの case of のネストはなんとかならないものか
ranking' :: Hand -> HandRank
ranking' ha@((Card J _):cs) = last $ sort $ map ranking $ map (\ca -> sort $ ca:cs) (wildcard cs)
ranking' ha = case maybeFiveCard ha of
  Just ra -> FiveCard ra
  Nothing -> case maybeRoyalStraightFlush ha of
    Just su -> RoyalStraightFlush su
    Nothing -> case maybeStraightFlush ha of
      Just ca -> StraightFlush ca
      Nothing -> case maybeFourCard ha of
        Just ra -> FourCard ra
        Nothing -> case maybeFullHouse ha of
          Just ra -> FullHouse ra
          Nothing -> case maybeFlush ha of
            Just ca -> Flush ca
            Nothing -> case maybeStraight ha of
              Just ca -> Straight ca
              Nothing -> case maybeThreeCard ha of
                Just ra -> ThreeCard ra
                Nothing -> case maybeTwoPair ha of
                  Just (ra1, ra2, ca) -> TwoPair ra1 ra2 ca
                  Nothing             -> case maybeOnePair ha of
                    Just (ra, ca) -> OnePair ra ca
                    Nothing       -> HighCard $ ha !! 4

maybeFiveCard :: Hand -> Maybe Rank
maybeFiveCard ha@((Card B _):cs) = maybeFourCard ha
maybeFiveCard _                  = Nothing

maybeRoyalStraightFlush :: Hand -> Maybe Suit
maybeRoyalStraightFlush ha@((Card su ra):cs) = case maybeStraightFlush ha of
  Nothing -> Nothing
  Just ca -> if ra == Ten then Just su else Nothing

maybeStraightFlush :: Hand -> Maybe Card
maybeStraightFlush ha = case maybeFlush ha of
  Nothing -> Nothing
  Just ca -> case maybeStraight ha of
    Nothing -> Nothing
    Just _  -> Just ca

maybeFourCard :: Hand -> Maybe Rank
maybeFourCard ha
  | all (1 ==) $ zipWith (-) (tail ha1) ha1 = Just $ rank $ ha !! 4
  | all (1 ==) $ zipWith (-) (tail ha2) ha2 = Just $ rank $ ha !! 0
  | otherwise                               = Nothing
  where ha1 = map fromEnum $ tail ha
        ha2 = map fromEnum $ init ha

maybeFullHouse :: Hand -> Maybe Rank
maybeFullHouse (Card _ ra1:Card _ ra2:Card _ ra3:Card _ ra4:[Card _ ra5])
  | ra1 == ra2 && ra1 == ra3 && ra4 == ra5 = Just ra1
  | ra1 == ra2 && ra3 == ra4 && ra3 == ra5 = Just ra3
  | otherwise                              = Nothing
maybeFullHouse ha = error $ show ha

maybeFlush :: Hand -> Maybe Card
maybeFlush ((Card B _):_) = Nothing
maybeFlush ha             = if all (\ca -> nu1 == num ca) (tail ha) then Just (ha !! 4) else Nothing
  where num ca  = (fromEnum ca - 2) `mod` 4
        nu1 = num $ ha !! 0

maybeStraight :: Hand -> Maybe Card
maybeStraight ((Card B _):_)          = Nothing
maybeStraight (ca1:ca2:ca3:ca4:[ca5]) = if isStraight [num ca1, num ca2, num ca3, num ca4, num ca5] then Just ca5 else Nothing
  where num ca = (fromEnum ca - 2) `div` 4
isStraight :: [Int] -> Bool
isStraight ns = all (1 ==) (zipWith (-) (tail ns) ns) || ns == [0, 1, 2, 3, 12]

maybeThreeCard :: Hand -> Maybe Rank
maybeThreeCard (Card _ ra1:Card _ ra2:Card _ ra3:Card _ ra4:[Card _ ra5])
  | ra1 == ra2 && ra1 == ra3 = Just ra1
  | ra2 == ra3 && ra2 == ra4 = Just ra2
  | ra3 == ra4 && ra3 == ra5 = Just ra3
  | otherwise                = Nothing

maybeTwoPair :: Hand -> Maybe (Rank, Rank, Card)
maybeTwoPair (ca1@(Card _ ra1):Card _ ra2:ca3@(Card _ ra3):Card _ ra4:[ca5@(Card _ ra5)])
  | ra1 == ra2 && ra3 == ra4 = if ra2 > ra4 then Just (ra2, ra4, ca5) else Just (ra4, ra2, ca5)
  | ra1 == ra2 && ra4 == ra5 = if ra2 > ra5 then Just (ra2, ra5, ca3) else Just (ra5, ra2, ca3)
  | ra2 == ra3 && ra4 == ra5 = if ra3 > ra5 then Just (ra3, ra5, ca1) else Just (ra5, ra3, ca1)
  | otherwise                = Nothing

maybeOnePair :: Hand -> Maybe (Rank, Card)
maybeOnePair (Card _ ra1:Card _ ra2:ca3@(Card _ ra3):Card _ ra4:[ca5@(Card _ ra5)])
  | ra1 == ra2 = Just (ra2, ca5)
  | ra2 == ra3 = Just (ra3, ca5)
  | ra3 == ra4 = Just (ra4, ca5)
  | ra4 == ra5 = Just (ra5, ca3)
  | otherwise  = Nothing

パターンマッチがまだ結構多い。

例えばスリーカードなら、ソート済みなので [2, 2, 2, 3, 4]、[2, 3, 3, 3, 4]、[2, 3, 4, 4, 4] のような先頭か真ん中か後ろに同じ番号が固まっているパターンしかないんだけど、これを

  | ra1 == ra2 && ra1 == ra3 = Just ra1
  | ra2 == ra3 && ra2 == ra4 = Just ra2
  | ra3 == ra4 && ra3 == ra5 = Just ra3
  | otherwise                = Nothing

のように力技で判定している。リスト操作で 3 枚あるカードがそれぞれ 2 である、3 である、4 である、ということがわかるようには書けないものか。

2014/08/22 追記

Data.List の group を使うと [2, 2, 2, 3, 4]、[2, 3, 3, 3, 4]、[2, 3, 4, 4, 4] がそれぞれ [ [2, 2, 2], [3], [4] ]、[ [2], [3, 3, 3], [4] ]、[ [2], [3], [4, 4, 4] ] のような形になるので、以下のように書き換えられる。

-- 元実装
maybeFullHouse (Card _ ra1:Card _ ra2:Card _ ra3:Card _ ra4:[Card _ ra5])
  | ra1 == ra2 && ra1 == ra3 && ra4 == ra5 = Just ra1
  | ra1 == ra2 && ra3 == ra4 && ra3 == ra5 = Just ra3
  | otherwise                              = Nothing

-- case of で書きなおしたもの
-- Just の使い方が不自然だしパターンマッチが汚い
maybeFullHouse (Card _ ra1:Card _ ra2:Card _ ra3:Card _ ra4:[Card _ ra5]) =
  case Just (group [ra1, ra2, ra3, ra4, ra5]) of
    Just ((ra:_:[_]):[_]) -> Just ra
    Just (_:([ra:_:[_]])) -> Just ra
    Just _                -> Nothing

-- if then else で書きなおしたもの
-- if else のネストが美しくない
maybeFullHouse (Card _ ra1:Card _ ra2:Card _ ra3:Card _ ra4:[Card _ ra5]) =
  if length l /= 2 then Nothing
    else if length l1 == 3 then Just $ l1 !! 0
      else Just $ l2 !! 0
  where l = group [ra1, ra2, ra3, ra4, ra5]
        l1 = l !! 0
        l2 = l !! 1

書きなおした実装がどちらもあまり良くない。