繁体   English   中英

如何让这段Haskell代码更简洁?

[英]How make this piece of Haskell code more concise?

作为练习,我正在尝试为Haskell中的赌场游戏“战争”编写模拟。

http://en.wikipedia.org/wiki/Casino_war

这是一个非常简单的游戏,有一些规则。 用我所知道的任何命令式语言编写一个非常简单的问题,但是我很难在Haskell中编写它。

我到目前为止的代码:

 -- Simulation for the Casino War

import System.Random
import Data.Map

-------------------------------------------------------------------------------
-- stolen from the internet

fisherYatesStep :: RandomGen g => (Map Int a, g) -> (Int, a) -> (Map Int a, g)
fisherYatesStep (m, gen) (i, x) = ((insert j x . insert i (m ! j)) m, gen')
    where
        (j, gen') = randomR (0, i) gen

fisherYates :: RandomGen g => g -> [a] -> ([a], g)
fisherYates gen [] = ([], gen)
fisherYates gen l = toElems $ Prelude.foldl
        fisherYatesStep (initial (head l) gen) (numerate (tail l))
    where
        toElems (x, y) = (elems x, y)
        numerate = zip [1..]
        initial x gen = (singleton 0 x, gen)

-------------------------------------------------------------------------------

data State = Deal | Tie deriving Show

-- state: game state
-- # cards to deal
-- # cards to burn
-- cards on the table
-- indices for tied players
-- # players
-- players winning
-- dealer's winning
type GameState = (State, Int, Int, [Int], [Int], Int, [Int], Int)

gameRound :: GameState -> Int -> GameState
gameRound (Deal, toDeal, toBurn, inPlay, tied, numPlayers, pWins, dWins) card
    | toDeal > 0 =
        -- not enough card, deal a card
        (Deal, toDeal - 1, 0, card:inPlay, tied, numPlayers, pWins, dWins)
    | toDeal == 0 =
        -- enough cards in play now
        -- here should detemine whether or not there is any ties on the table,
        -- and go to the tie state
        let
            dealerCard = head inPlay
            p = zipWith (+) pWins $ (tail inPlay) >>=
                (\x -> if x < dealerCard then return (-1) else return 1)
            d = if dealerCard == (maximum inPlay) then dWins + 1 else dWins - 1
        in
            (Deal, numPlayers + 1, 0, [], tied, numPlayers, p, d)
gameRound (Tie, toDeal, toBurn, inPlay, tied, numPlayers, pWins, dWins) card
    -- i have no idea how to write the logic for the tie state AKA the "war" state
    | otherwise = (Tie, toDeal, toBurn, inPlay, tied, numPlayers, pWins, dWins)

-------------------------------------------------------------------------------

main = do
    rand <- newStdGen
    -- create the shuffled deck
    (deck, _) <- return $ fisherYates rand $ [2 .. 14] >>= (replicate 6)
    -- fold the state updating function over the deck
    putStrLn $ show $ Prelude.foldl gameRound
        (Deal, 7, 0, [], [], 6, [0 ..], 0) deck

-------------------------------------------------------------------------------

我理解为什么额外的工作必须用于创建随机数,但我很确定我缺少一些基本的构造或概念。 保持状态集合并在输入列表上运行分支逻辑不应该是这样的尴尬。 我甚至无法找到一种很好的方法来为表格上存在联系的情况编写逻辑。

我不是要求完整的解决方案。 如果有人可以指出我做错了什么,或者一些相关的好阅读材料,那将是非常好的。

提前致谢。

用于维护应用程序状态的有用设计模式是所谓的状态monad。 您可以在此处找到说明和一些介绍性示例。 此外,您可能需要考虑使用带有命名字段的数据类型而不是GameState的元组,例如:

data GameState = GameState { state :: State, 
                             toDeal :: Int
                           -- and so on
                           }

这将使用记录语法更容易访问/更新单个字段。

为了使代码更具可读性,您应该将游戏结构分解为有意义的组件,并相应地重新组织代码。 你所做的就是把所有游戏的状态都放在一个数据结构中。 结果是您必须始终处理所有游戏细节。

游戏记录每个玩家和经销商的得分。 有时它会从分数中加1或减1。 分数不用于其他任何事情。 将得分管理与其他代码分开:

-- Scores for each player and the dealer
data Score = Score [Int] Int

-- Outcome for each player and the dealer.  'True' means a round was won.
data Outcome = Outcome [Bool] Bool

startingScore :: Int -> Score
startingScore n = Score (replicate n 0) 0

updateScore :: Outcome -> Score -> Score
updateScore (Outcome ps d) (Score pss ds) = Score (zipWith upd pss pos) (update ds d)
  where upd s True  = s+1
        upd s False = s-1

发出的牌也与玩家和经销商有关。 赢或输一轮仅基于卡值。 将得分计算与其他代码分开:

type Card = Int
data Dealt = Dealt [Card] Card

scoreRound :: Dealt -> Outcome
scoreRound (Dealt ps dealerCard) = Outcome (map scorePlayer ps) (dealerCard == maximumCard)
  where
    maximumCard = maximum (dealerCard : ps)
    scorePlayer p = p >= dealerCard

我会说游戏回合包含产生单一Outcome所需的所有步骤。 相应地重新组织代码:

type Deck = [Card]

deal :: Int -> Deck -> (Dealt, Deck)
deal n d = (Dealt (take n d) (head $ drop n d), drop (n+1) d) -- Should check whether deck has enough cards

-- The 'input-only' parts of GameState
type GameConfig =
  GameConfig {nPlayers :: Int}

gameRound :: GameConfig -> Deck -> (Deck, Outcome)
gameRound config deck = let
  (dealt, deck') = deal (nPlayers config) deck
  outcome        = scoreRound dealt
  in (deck', outcome)

这涵盖了原始代码中的大部分内容。 您可以以类似的方式接近其余部分。


你应该得到的主要想法是,Haskell可以很容易地将程序分解成自己有意义的小块 这就是使代码更易于使用的原因。

我创造了ScoreOutcomeDealtDeck ,而不是将所有内容都放入GameState 其中一些数据类型来自原始的GameState 其他人根本不在原始代码中; 它们隐含在组织复杂循环的方式中。 我没有将整个游戏放入gameRound ,而是创建了updateScorescoreRounddeal和其他功能。 这些中的每一个仅与少量数据交互。

在我看来,建议'使用StateT'可能有点不透明,所以我翻译了一下这个术语,希望你能看到如何去那里。 最好在游戏状态中包含套牌的状态。 下面的gameround只是在StateT术语中重述你的功能。 之前的定义, game使用游戏状态的deck场,不断减少,并包含整个游戏。 我介绍了IO操作,只是为了说明它是如何完成的,所以如果你在ghci中调用main,你就可以看到状态的连续性。 你将IO动作提升到StateT机器中,将它们放在获得和放置的水平上。 请注意,在mose子句中,我们将新状态设置为然后调用以重复操作,以便do块包含完整的递归操作。 (铁和一个空的甲板立即结束游戏。)然后,在最后一行main我们runStateT在此自更新game得到的函数游戏状态- > IO(游戏状态,()); 然后我们用一个特定的起始状态来提供它,包括随机确定的牌组,以获得作为主要业务的IO动作。 (我不遵循游戏应该如何运作,而是机械地移动东西以实现这个想法。)

import Control.Monad.Trans.State
import Control.Monad.Trans
import System.Random
import Data.Map

data Stage = Deal | Tie deriving Show
data GameState = 
  GameState   { stage      :: Stage
              , toDeal     :: Int
              , toBurn     :: Int
              , inPlay     :: [Int]
              , tied       :: [Int]
              , numPlayers :: Int
              , pWins      :: [Int]
              , dWins      :: Int
              , deck      ::  [Int]} deriving Show
              -- deck field is added for the `game` example
type GameRound m a = StateT GameState m a

main = do
   rand <- newStdGen
   let deck = fst $ fisherYates rand $ concatMap (replicate 6) [2 .. 14] 
   let startState = GameState Deal 7 0 [] [] 6 [0 ..100] 0 deck
   runStateT game startState 

game  ::   GameRound IO ()
game = do
  st <- get
  lift $ putStrLn "Playing: " >> print st
  case deck st of 
    []            -> lift $ print "no cards"
    (card:cards)  -> 
      case (toDeal st, stage st) of 
        (0, Deal) ->  do put (first_case_update st card cards) 
                         game -- <-- recursive call with smaller deck
        (_, Deal) ->  do put (second_case_update st card cards)
                         game
        (_,  Tie) ->  do lift $ putStrLn "This is a tie"
                         lift $ print st

 where    -- state updates:
          -- I separate these out hoping this will make the needed sort 
          -- of 'logic' above clearer.
  first_case_update s card cards= 
     s { numPlayers = numPlayers s + 1
       , pWins = [if x < dealerCard  then -1 else  1 |
                    x <-  zipWith (+) (pWins s)  (tail (inPlay s)) ]
       , dWins = if dealerCard == maximum (inPlay s) 
                     then dWins s + 1 
                     else dWins s - 1
       , deck = cards }
            where  dealerCard = head (inPlay s)

  second_case_update s card cards = 
     s { toDeal = toDeal s - 1 
       , toBurn = 0 
       , inPlay = card : inPlay s
       , deck = cards}

--  a StateTified formulation of your gameRound
gameround  ::  Monad m => Int -> GameRound m ()
gameround card = do
  s <- get
  case (toDeal s, stage s) of 
    (0, Deal) -> 
        put $ s { toDeal = numPlayers s + 1
                , pWins = [if x < dealerCard  then -1 else  1 |
                             x <-  zipWith (+) (pWins s)  (tail (inPlay s)) ]
                , dWins = if dealerCard == maximum (inPlay s) 
                              then dWins s + 1 
                              else dWins s - 1}
                     where  dealerCard = head (inPlay s)
    (_, Deal) -> 
        put $ s { toDeal = toDeal s - 1 
                 , toBurn = 0 
                 , inPlay = card : inPlay s}
    (_,  Tie) -> return ()


fisherYatesStep :: RandomGen g => (Map Int a, g) -> (Int, a) -> (Map Int a, g)
fisherYatesStep (m, gen) (i, x) = ((insert j x . insert i (m ! j)) m, gen')
    where
        (j, gen') = randomR (0, i) gen

fisherYates :: RandomGen g => g -> [a] -> ([a], g)
fisherYates gen [] = ([], gen)
fisherYates gen l = toElems $ Prelude.foldl
        fisherYatesStep (initial (head l) gen) (numerate (tail l))
    where
        toElems (x, y) = (elems x, y)
        numerate = zip [1..]
        initial x gen = (singleton 0 x, gen)       

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM