我正在尝试使用Haskell创建一个Paper Scissors Stone游戏来练习我对它的理解。
不幸的是,下面的源代码会给出不必要的答案。
例如:
>play pStone pRandom 1
1 games were played. Player 1 won 1 and player 2 won 1, making the match a draw.
如果玩1场比赛,那么应该只有1或0胜。
>play pStone pCopy 100
100 games were played. Player 1 won 1 and player 2 won 1, making the match a draw.
如果参加了100场比赛,那么(第一轮之后)两场比赛都进行同样的比赛,那么应该只有1场或0场胜利。
>play pCopy pAntiCopy 100
100 games were played. Player 1 won 31 and player 2 won 37, making player 2 the overall winner.
根据pCopy和pAntiCopy的预期定义,pAntiCopy应该赢得99或100,pCopy应该赢得0或1。
我认为这种行为的最可能原因是随机数在最后被评估,这意味着应该依赖于相同随机数的2个值依赖于2个单独的随机数。我在上面是否正确?
如果我是对的,请你告诉我应该如何纠正这个问题?如果我不正确,请告诉我问题是什么以及如何解决?
我已经阅读了一个单独问题的解决方案,该问题建议生成一个随机数列表然后使用它们,将它们作为参数传递给主函数中的相关函数。我不相信这会在这里工作得很好,因为所需的随机数的数量可以是0到2 * numRounds,取决于所使用的计划(我打算在这个工作时创建更高级的计划)和代码的可读性会进一步降低。
我是Haskell的新手和一般的函数式编程,所以我为下面的源代码风格道歉。如果您对如何改进有任何建议,也欢迎他们。
import System.Random
data Move= Paper|Scissors|Stone deriving Show
type Plan=([IO Move]->[IO Move]->IO Move)
play :: Plan -> Plan -> Integer -> IO ()
play plan1 plan2 numRounds=do p1intwins<-p1wins;p2intwins<-p2wins;putStr(show numRounds ++ " games were played. Player 1 won " ++ show p1intwins ++ " and player 2 won " ++ show p2intwins ++ ", making " ++ (if p1intwins > p2intwins then "player 1 the overall winner." else (if p1intwins < p2intwins then "player 2 the overall winner." else "the match a draw."))) where (_, _, _, _, _, _, p1wins, p2wins)=(playRound (plan1, plan2, numRounds,[],[], 0, return 0, return 0))
playRound :: (Plan, Plan, Integer, [IO Move], [IO Move], Integer, IO Integer, IO Integer) -> (Plan, Plan, Integer, [IO Move], [IO Move], Integer, IO Integer, IO Integer)
playRound (plan1, plan2, numRounds, p1moves, p2moves, elapsedRounds, p1wins, p2wins)=if elapsedRounds==numRounds then (plan1, plan2, numRounds, p1moves, p2moves, elapsedRounds, p1wins, p2wins) else (playRound (plan1, plan2, numRounds, p1moves++[p1move], p2moves++[p2move], elapsedRounds+1, do p1win<-beatsCaller p1move p2move;p1intwins<-p1wins;return (p1intwins+if p1win then 1 else 0), do p2win<-beatsCaller p2move p1move;p2intwins<-p2wins;return(p2intwins+(if p2win then 1 else 0)) )) where p1move=plan1 p1moves p2moves; p2move=plan2 p2moves p1moves
beatsCaller :: IO Move -> IO Move -> IO Bool
beatsCaller iom1 iom2=do m1<-iom1;m2<-iom2;return(beats m1 m2)
beats :: Move -> Move -> Bool
beats Scissors Paper=True
beats Stone Scissors=True
beats Paper Stone=True
beats _ _=False
-- ###############Plans###################
pStone :: Plan
pStone _ _ = return Stone
pScissors :: Plan
pScissors _ _ = return Scissors
pPaper :: Plan
pPaper _ _ = return Paper
pUScissors :: Plan
pUScissors [] _ = randomMove
pUScissors _ _ = return Scissors
pCopy :: Plan
pCopy _ []= randomMove
pCopy _ theirMoves= last theirMoves
pRandom :: Plan
pRandom _ _=randomMove
pAntiCopy :: Plan
pAntiCopy [] _ = randomMove
pAntiCopy ourMoves _ = do ourMove<-last ourMoves;return(beaterOf ourMove)
-- ##############Plan Logic###############
beaterOf :: Move -> Move
beaterOf Scissors = Stone
beaterOf Paper = Scissors
beaterOf Stone = Paper
randomMove :: IO Move
randomMove=do x<-genRandom;return (doRMove x)
doRMove:: Int -> Move
doRMove rand
|rand==1 =Paper
|rand==2 =Scissors
|rand==3 =Stone
genRandom :: IO Int
genRandom =getStdRandom (randomR (1,3))
答案 0 :(得分:6)
我稍微重新格式化并注释了您的源文件。关键点是,您应该将Plan
的类型更改为[Move] -> [Move] -> IO Move
,如下所述。
import System.Random
data Move = Paper | Scissors | Stone
deriving (Show, Eq, Enum, Bounded)
-- Make Move an instance of Random, so that we can use randomIO
-- in order to pick a random Move rather than a hand-written
-- function. We use the derived Enum instance and integer random
-- numbers to do the hard work for us.
instance Random Move where
randomR (l, u) g = (toEnum n, g')
where (n, g') = randomR (fromEnum l, fromEnum u) g
random = randomR (minBound, maxBound)
-- Unchanged, just realigned.
beaterOf :: Move -> Move
beaterOf Scissors = Stone
beaterOf Paper = Scissors
beaterOf Stone = Paper
-- Reimplemented in terms of beaterOf, to avoid unnecessary
-- duplication of error-prone information.
beats :: Move -> Move -> Bool
beats x y = x == beaterOf y
-- Most important change. A plan is a strategy that takes two
-- lists of past moves. These are of type Move, not IO Move, as
-- they have already happened. We then have to determine a new
-- one. Here, to choose the next, we allow IO (such as picking
-- a random number, or asking a human player). I also reverse
-- the order of moves, so that the most recent moves are first,
-- because most strategies access the most recent move, and
-- accessing the head is more efficient in a list.
type Plan = [Move] -- my moves, latest move first
-> [Move] -- opponent's moves, latest move first
-> IO Move -- my next move, may involve IO
--
-- Specific plans (slightly renamed, otherwise unchanged):
--
-- Always plays a particular move.
always :: Move -> Plan
always m _ _ = return m
-- Copies the latest move of opponent.
copy :: Plan
copy _ [] = randomIO
copy _ (latest : _) = return latest
randomly :: Plan
randomly _ _ = randomIO
-- Moves to the beater of our previous move.
antiCopy :: Plan
antiCopy [] _ = randomIO
antiCopy (latest : _) _ = return (beaterOf latest)
uScissors :: Plan
uScissors [] _ = randomIO
uScissors _ _ = return Scissors
-- Play wrapper. Interface as before.
play :: Plan -- my plan
-> Plan -- opponent's plan
-> Integer -- number of rounds to be played
-> IO () -- output is printed as text
play myPlan opPlan rounds = do
(myWins, opWins) <- playRounds
myPlan opPlan rounds
[] [] -- initialize with empty move lists
0 0 -- and 0 wins each
-- print statistics
let overallStatus | myWins > opWins = "Player 1 the overall winner"
| opWins > myWins = "Player 2 the overall winner"
| otherwise = "the match a draw"
putStrLn $ show rounds ++ " games were played. "
++ "Player 1 won " ++ show myWins ++ " and "
++ "Player 2 won " ++ show opWins ++ ", making "
++ overallStatus ++ "."
-- Does all the hard work.
playRounds :: Plan -- my plan
-> Plan -- opponent's plan
-> Integer -- number of rounds still to be played
-> [Move] -- our moves so far, latest first
-> [Move] -- opponent's moves so far, latest first
-> Int -- my wins
-> Int -- opponent's wins
-> IO (Int, Int) -- final wins
playRounds _ _ 0 _ _ myWins opWins =
return (myWins, opWins) -- if no rounds are left to play, return the final statistics
playRound myPlan opPlan rounds myMoves opMoves myWins opWins = do
myMove <- myPlan myMoves opMoves -- here's where a random number might get chosen
opMove <- opPlan opMoves myMoves -- and here again (but nowhere else)
let myWin = myMove `beats` opMove -- this works on the already chosen Move, not on IO Move
opWin = opMove `beats` myMove -- dito
playRound
myPlan opPlan -- as before
(rounds - 1) -- one round is played
(myMove : myMoves) -- adding moves in front of the list is easier
(opMove : opMoves)
(fromEnum myWin + myWins) -- update win count, turning True to 1 and False to 0
(fromEnum opWin + opWins)
答案 1 :(得分:4)
我认为这种行为的最可能原因是随机数在最后被评估,这意味着应该依赖于相同随机数的2个值依赖于2个单独的随机数。我在上面是否正确?
你是。由于你实际上并没有在迭代期间玩,而是通过IO
- 动作,你的策略没有普通的Move
- 前几轮的结果 - 要构建,但生成的方法一个Move
,在某些情况下涉及执行genRandom
以获得Move
。每次都在
ourMove<-last ourMoves
last ourMoves
涉及一个genRandom
,会产生一个新的(伪)随机数 - 通常与产生前一个Move
的那个不同。
如果我是对的,请你告诉我如何纠正这个问题?
不要传递IO
行动和IO
- 使用genRandom
周围的行动列表。您需要纯值来确定策略(pRandom
以及pCopy
和pAntiCopy
中的初始选项除外)。
在必要时在每个步骤中运行伪随机数生成,并将获得的纯Move
传递给下一次迭代。
此外,使用较短的行,使用空格,并使用布局使代码更具可读性。
我将以更惯用的方式重写它,但我是一个慢打字员,所以它可能需要一段时间。
好吧,它仍然有点混乱,因为我保留了很多原始输出和逻辑,但它更容易阅读和遵循,并产生预期的输出:
module Rocks where
import System.Random
data Move
= Paper
| Scissors
| Stone
deriving Show
type Plan = [Move] -> [Move] -> IO Move
play :: Plan -> Plan -> Int -> IO ()
play plan1 plan2 numRounds = do
(wins1, wins2) <- playRounds plan1 plan2 numRounds [] [] 0 0
putStrLn $ show numRounds ++ " games were played. Player 1 won "
++ show wins1 ++ " and player 2 won " ++ show wins2
++ ", making " ++ result wins1 wins2
where
result a b
| a == b = "the match a draw."
| otherwise = "player " ++ (if a > b then "1" else "2") ++ " the overall winner."
playRounds :: Plan -> Plan -> Int -> [Move] -> [Move] -> Int -> Int -> IO (Int, Int)
playRounds _ _ 0 _ _ wins1 wins2 = return (wins1,wins2)
playRounds plan1 plan2 rounds moves1 moves2 wins1 wins2 = do
choice1 <- plan1 moves1 moves2
choice2 <- plan2 moves2 moves1
let (w1, w2)
| beats choice1 choice2 = (wins1+1 ,wins2)
| beats choice2 choice1 = (wins1, wins2+1)
| otherwise = (wins1, wins2)
playRounds plan1 plan2 (rounds-1) (moves1 ++ [choice1]) (moves2 ++ [choice2]) w1 w2
beats :: Move -> Move -> Bool
beats Scissors Paper = True
beats Stone Scissors = True
beats Paper Stone = True
beats _ _ = False
-- ###############Plans###################
pStone :: Plan
pStone _ _ = return Stone
pScissors :: Plan
pScissors _ _ = return Scissors
pPaper :: Plan
pPaper _ _ = return Paper
pUScissors :: Plan
pUScissors [] _ = randomMove
pUScissors _ _ = return Scissors
pCopy :: Plan
pCopy _ [] = randomMove
pCopy _ theirMoves = return $ last theirMoves
pRandom :: Plan
pRandom _ _ = randomMove
pAntiCopy :: Plan
pAntiCopy [] _ = randomMove
pAntiCopy ourMoves _ = return (beaterOf $ last ourMoves)
-- ##############Plan Logic###############
beaterOf :: Move -> Move
beaterOf Scissors = Stone
beaterOf Paper = Scissors
beaterOf Stone = Paper
randomMove :: IO Move
randomMove = fmap doRMove genRandom
doRMove:: Int -> Move
doRMove rand
| rand == 1 = Paper
| rand == 2 = Scissors
| rand == 3 = Stone
| otherwise = error "oops"
genRandom :: IO Int
genRandom = getStdRandom (randomR (1,3))
与
*Rocks> play pCopy pAntiCopy 100
100 games were played. Player 1 won 0 and player 2 won 99, making player 2 the overall winner.
*Rocks> play pStone pRandom 100
100 games were played. Player 1 won 33 and player 2 won 34, making player 2 the overall winner.
*Rocks> play pStone pCopy 100
100 games were played. Player 1 won 1 and player 2 won 0, making player 1 the overall winner.
*Rocks> play pStone pAntiCopy 100
100 games were played. Player 1 won 33 and player 2 won 33, making the match a draw.