作为练习,我正在尝试为赌场游戏" war"在哈斯克尔。
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
-------------------------------------------------------------------------------
我理解为什么额外的工作必须用于创建随机数,但我很确定我缺少一些基本的构造或概念。保持状态集合并在输入列表上运行分支逻辑不应该是这样的尴尬。我甚至无法找到一种很好的方法来为桌面上有联系的情况编写逻辑。
我不是要求完整的解决方案。如果有人能够指出我做错了什么,或者是一些相关的好的阅读材料,那将是非常好的。
提前致谢。
答案 0 :(得分:6)
用于维护应用程序状态的有用设计模式是所谓的状态monad。您可以找到说明和一些介绍性示例here。此外,您可能需要考虑使用带有命名字段的数据类型而不是GameState
的元组,例如:
data GameState = GameState { state :: State,
toDeal :: Int
-- and so on
}
这样可以更轻松地使用record syntax访问/更新单个字段。
答案 1 :(得分: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
,Score
,Outcome
和Dealt
,而不是将所有内容都放入Deck
。其中一些数据类型来自原始GameState
。其他人根本不在原始代码中;它们隐含在组织复杂循环的方式中。我没有将整个游戏放入gameRound
,而是创建了updateScore
,scoreRound
,deal
和其他功能。这些中的每一个都只与少量数据相互作用。
答案 2 :(得分:2)
我突然想到“使用StateT”这个建议可能有点不透明,所以我翻译了一下这个术语,希望你能看到如何去那里。最好在游戏状态中包含套牌的状态。下面的gameround
只是在StateT术语中重述您的功能。之前的定义game
使用游戏状态的deck
字段,不断缩小,并包含整个游戏。我介绍了IO操作,只是为了说明它是如何完成的,所以如果你在ghci中调用main,你就可以看到状态的连续性。你将IO动作提升到StateT机器中,将它们放在获得和放置的水平上。请注意,在mose子句中,我们将新状态设置为然后调用以重复操作,以便do块包含完整的递归操作。 (领带和一个空的牌组立即结束游戏。)然后在main
的最后一行,我们runStateT
在这个自我更新game
上产生一个函数GameState - &gt; 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)