Just $ A sandbox

プログラミングと計算機科学とかわいさ

Haskellで大富豪を作ろう (3)カードの組を判定しよう

前回までの記事

第三回 カードの組を判定しよう

前回はカードを捨てられるようになったので, 次は同じ数字は組として一緒に出せるようにしよう.
ところで, 例えば同じカードが3枚あればそのうち好きな1枚または2枚を出すこともできるので, 何を組にして出すかはユーザーに選択させるようにしよう.

また, 左から何枚目のカードを捨てるというのは分かりにくいので, どの数字を捨てるかを入力させることにしよう.

結果として, 自分のターンには,

  1. 何の数字のカードを出すか選択
  2. 同じ数字のカードが1枚しかない場合はそれを出す. 2枚以上ある場合はカードを表示して, どのカードを出すかを選択させる

の操作を行うことにした.

player :: StateT Game IO [Card]
player = do
  ds <- use (decks . ix 0)
  lift $ print $ groupBy sameNum ds
  lift $ putStrLn "出すカードを選択してください. (ジョーカーはJ)"
  str <- lift getLine
  case pickWith str ds of
    Just ps -> process ds ps
    Nothing -> do
      lift $ putStrLn "正しいカードNoを選択してください."
      player

  where
    process :: [Card] -> [Card] -> StateT Game IO [Card]
    process ds ps = do
      case length ps == 1 of
        True -> do
          decks %= IM.insert 0 (ds \\ ps)
          return ps
        False -> whichToDiscard ds ps

次回は大富豪として遊べるようになるために, カードの出し方やパスといった基本的なルールを組み込もう.

実行例

Let's play 大富豪!
あなたの番です.
[[♣3,♥3],[♠5],[♥7,♣7],[♥8],[♦K,♣K],[♦2,♥2,♠2],[Joker 0],[Joker 1]]
出すカードを選択してください. (ジョーカーはJ)
> 3
0:♣3 1:♥3
どのカードを出しますか? (複数の場合はスペースで区切る)
> 0 1
場札:[♣3,♥3]
CPU1さんの番です.
場札:♠3
CPU2さんの番です.
場札:♦3
CPU3さんの番です.
場札:♠4
あなたの番です.
[[♠5],[♥7,♣7],[♥8],[♦K,♣K],[♦2,♥2,♠2],[Joker 0],[Joker 1]]
出すカードを選択してください. (ジョーカーはJ)
> J
0:Joker 0 1:Joker 1
どのカードを出しますか? (複数の場合はスペースで区切る)
> 0
場札:[Joker 0]
CPU1さんの番です.
場札:♣4
CPU2さんの番です.
場札:♣5
CPU3さんの番です.
場札:♦4
あなたの番です.
[[♠5],[♥7,♣7],[♥8],[♦K,♣K],[♦2,♥2,♠2],[Joker 1]]
出すカードを選択してください. (ジョーカーはJ)

使用したパッケージ

ソースコード

{-# LANGUAGE TemplateHaskell #-}
module Main where
 
import Control.Lens
import Control.Monad.State
import Data.Char
import Data.List
import qualified Data.IntMap as IM
import Data.List.Split (chunksOf)
import System.Random.Shuffle (shuffleM)
 
-- (3) カードの組を判定しよう
 
data Suit = Spade | Club | Diamond | Heart deriving (Eq, Enum)
data Number = N3 | N4 | N5 | N6 | N7 | N8 | N9 | N10 | N11 | N12 | N13 | N1 | N2 deriving (Eq, Ord, Enum)
data JokerNumber = J1 | J2 deriving (Eq, Ord, Enum, Show)
data Card = Card Suit Number | Joker JokerNumber deriving Eq
 
allCards :: [Card]
allCards = [Card s n | s <- [Spade .. Heart], n <- [N3 .. N2]] ++ [Joker j | j <- [J1,J2]]
 
isNormalCard :: Card -> Bool
isNormalCard (Card _ _) = True
isNormalCard _ = False
 
isJoker :: Card -> Bool
isJoker (Card _ _) = False
isJoker _ = True
 
toNumber :: Int -> Number
toNumber n
  | 3 <= n && n <= 13 = toEnum $ n - 3
  | n == 1 || n == 2 = toEnum $ n + 10
  | otherwise = toNumber (n `mod` 13)
 
fromNumber :: Number -> Int
fromNumber n
  | n <= N13 = fromEnum n + 3
  | otherwise = fromEnum n - 10
 
instance Show Suit where
  show Spade = "♠"
  show Club = "♣"
  show Diamond = "♦"
  show Heart = "♥"
 
instance Show Number where
  show N1 = "A"
  show N11 = "J"
  show N12 = "Q"
  show N13 = "K"
  show k = show $ fromNumber k
 
instance Show Card where
  show (Card s n) = show s ++ show n
  show (Joker k) = "Joker " ++ show (fromEnum k)
 
instance Ord Card where
  Joker j <= Joker j' = j <= j'
  Card _ _ <= Joker _ = True
  Joker _ <= Card _ _ = False
  Card _ k <= Card _ k' = k <= k'
 
deal :: Int -> IO [[Card]]
deal n = fmap (fmap sort . take n . chunksOf (length allCards `div` n)) $ shuffleM allCards
 
data Game = Game {
  _decks :: IM.IntMap [Card],
  _turn :: Int,
  _players :: Int
  }
 
makeLenses ''Game
 
game :: StateT Game IO ()
game = do
  t <- use turn
 
  case t == 0 of
    True -> do
      lift $ putStrLn $ "あなたの番です."
      k <- player
      lift $ putStrLn $ "場札:" ++ show k
    False -> do
      lift $ putStrLn $ "CPU" ++ show t ++ "さんの番です."
      k <- auto
      lift $ putStrLn $ "場札:" ++ show k
 
  p <- use players
  turn %= nextTurn p
 
  game
 
  where
    player :: StateT Game IO [Card]
    player = do
      ds <- use (decks . ix 0)
      lift $ print $ groupBy sameNum ds
      lift $ putStrLn "出すカードを選択してください. (ジョーカーはJ)"
      str <- lift getLine
      case pickWith str ds of
        Just ps -> process ds ps
        Nothing -> do
          lift $ putStrLn "正しいカードNoを選択してください."
          player
 
      where
        process :: [Card] -> [Card] -> StateT Game IO [Card]
        process ds ps = do
          case length ps == 1 of
            True -> do
              decks %= IM.insert 0 (ds \\ ps)
              return ps
            False -> whichToDiscard ds ps
 
        whichToDiscard :: [Card] -> [Card] -> StateT Game IO [Card]
        whichToDiscard ds ps = do
          let psi = zip [0..] ps
          lift $ putStrLn $ unwords $ fmap (\(i,c) -> show i ++ ":" ++ show c) psi
          lift $ putStrLn "どのカードを出しますか? (複数の場合はスペースで区切る)"
          str <- words <$> lift getLine
          case pickThese str ps of
            Just cs -> do
              decks %= IM.insert 0 (ds \\ cs)
              return cs
            Nothing -> whichToDiscard ds ps
 
    auto :: StateT Game IO Card
    auto = do
      t <- use turn
      ds <- use (decks . ix t)
      decks %= IM.insert t (delete (ds !! 0) ds)
      return $ ds !! 0
 
    nextTurn p t | t == p-1 = 0
                 | otherwise = t+1
 
    pickWith :: String -> [Card] -> Maybe [Card]
    pickWith "J" ds = Just $ filter isJoker ds
    pickWith str ds = filter (isSameNumber (read str)) <$> check where
      check = if all isDigit str && 1 <= read str && read str <= 13
                  then Just ds else Nothing
 
    pickThese :: [String] -> [Card] -> Maybe [Card]
    pickThese ss ps = fmap snd . filter (\(i,_) -> i `elem` fmap read ss) . zip [0..] <$> check where
      check = if all (all isDigit) ss && all (\s -> 0 <= read s && read s <= length ps - 1) ss
                 then Just ps else Nothing
 
    isSameNumber :: Int -> Card -> Bool
    isSameNumber n x = isNormalCard x && cardNumber x == n where
      cardNumber (Card _ n) = fromNumber n
      cardNumber _ = -1
 
    sameNum :: Card -> Card -> Bool
    sameNum (Card _ k) (Card _ l) = k == l
    sameNum (Joker k) (Joker l) = k == l
    sameNum _ _ = False
 
main = do
  putStrLn "Let's play 大富豪!"
 
  let pl = 4
  d <- deal pl
  runStateT game $ Game (IM.fromList $ zip [0..pl-1] d) 0 pl