7

作为实践,我正在尝试为 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

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

我理解为什么要在创建随机数上做额外的工作,但我很确定我错过了一些基本的构造或概念。保持状态集合并在输入列表上运行分支逻辑应该不会那么尴尬。我什至想不出一个好方法来为桌子上有关系的情况编写逻辑。

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

提前致谢。

4

3 回答 3

6

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

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

这将使使用记录语法访问/更新各个字段变得更加容易。

于 2012-06-18T03:21:01.580 回答
3

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

游戏会记录每个玩家和庄家的得分。有时它会从分数中加 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 可以很容易地将程序分解成对它们自己有意义的小块。这就是使代码更易于使用的原因。

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

于 2012-06-19T13:53:42.940 回答
2

我突然想到“使用 StateT”的建议可能有点不透明,所以我把它翻译成那个行话,希望你能看到如何从那里开始。最好将牌组的状态包含在游戏状态中。 gameround下面只是用 StateT 术语重述您的功能。前面的定义,game使用deck游戏状态的字段,不断缩小,并包含整个游戏。我介绍 IO 动作,只是为了展示它是如何完成的,所以如果你在 ghci 中调用 main,你可以看到状态的连续性。您将 IO 操作“提升”到 StateT 机器中,使它们与 get 和 put 处于同一水平。请注意,在 mose 子案例中,我们放置新状态,然后调用要重复的操作,以便 do 块包含完整的递归操作。(平局和一副空牌立即结束游戏。)然后在main我们runStateT自我更新的最后一行game产生一个函数 GameState -> IO (GameState,()); 然后我们用一个特定的起始状态(包括随机确定的牌组)来提供它,以获得主要业务的 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)       
于 2012-06-18T19:41:34.607 回答