有没有办法使用MVar在haskell中制作此文字游戏?

时间:2019-06-26 14:14:57

标签: multithreading haskell

我正在尝试学习MVar,并且正在尝试制作以下文字游戏:

狩猎从100个生命点开始,然后是1级

1。一个功能会对生命值造成伤害(15,30) 2,另一个功能可以治愈生命点(5,10) 3.增加一级,因为玩家幸存 4.重复直到生命点达到0

生命点将是MVar整数,因此一个函数必须等待另一个函数完成更改其值(同步可变变量)

我要重复这2个功能,直到生命点达到0,然后显示玩家达到的级别

这是我到目前为止所得到的:

import Control.Monad
import Data.Char
import Control.Concurrent
import System.Random

lifePoints :: Integer
lifePoints = 100

damageCalculation :: StdGen -> MVar Integer -> IO Int
damageCalculation gen life = do
    let (randDano, newGen) = randomR (15,30) gen :: (Int, StdGen)
    a <- readMVar life
    let newLife = life - randDano
    a <- swapMVar newLife
    return a


main :: IO ()
main = do
    putStr "Welcome to Hunter of Monsters\n"
    putStr "Whats your name? \n"
    l <- getLine
    putStr "Character\n"
    putStrLn ("Name: " ++ l) 
    let life = lifePoints 
    putStrLn $ "Life Points: " ++ show life


    putStr "The hunt begins\n"
    a <- newEmptyMVar 
    forkIO $ do 
        putStr "Damage calculation\n"
        lifeMVar <- newMVar life
        gen <- getStdGen
        let rest = damageCalculation gen lifeMVar
        putStrLn $ " " ++ show rest
        putMVar a ()
    takeMVar a
    putStrLn "Game over!"

在获取新的随机数时遇到麻烦,只需重复一个值即可。 损坏功能很简单,但是尝试更改生命值时出现错误。 另外,如何重复这些功能,直到寿命点达到0? 玩家级别也需要成为MVar吗?

1 个答案:

答案 0 :(得分:2)

在编译之前,您的代码存在许多问题:

  • damageCalculation尝试将life用作MVar Integer(即,包含整数的存储位置的“令牌”)和Integer值本身 li>
  • 您仅向swapMVar提供了一个参数,它期望两个参数
  • 您混合了IntInteger类型
  • main中,您已经将damageCalculation视为纯函数而不是I / O操作

修复所有这些错误将使程序可以进行类型检查(请参见下面的清单1)。

但是,仍然存在许多错误。 首先代码:

a <- readMVar life
let newLife = a - randDano
a <- swapMVar life newLife
如果多个线程试图更新损坏,则

处于竞争状态。第一行读取MVar的值,但不执行任何形式的锁定或同步,然后第三行无条件地写入新生命(同时获取旧值的副本)。

例如,如果两个线程试图以以下交错方式分别扣除10点损坏,则会遇到问题:

Thread 1                   Thread 2
--------                   --------
a <- readMVar life                                     -- fetch 100 life
                           a <- readMVar life          -- fetch 100 life again
let newLife = ...                                      -- deduct 10 to get 90
a <- swapMVar life newLife                             -- save 90 in the MVar
                           let newLife = ...           -- deduct 10 to get 90
                           a <- swapMVar life newLife  -- save 90 in the MVar

相反,您要使用一对takeMVarputMVar这些函数,它们旨在提供自动同步:

Thread 1                   Thread 2
--------                   --------
a <- takeMVar life                                     -- fetch 100 life
                           a <- takeMVar life          -- nothing there, so block...
let newLife = ...          -- BLOCKING                 -- deduct 10 to get 90
a <- putMVar life newLife  -- BLOCKING                 -- save 90 in the MVar
                           -- WAKE UP                  -- fetch 90 from MVar
                           let newLife = ...           -- deduct 10 to get 80
                           a <- putMVar life newLife   -- save 80 in the MVar

my answer to your other question中,我建议使用readMVarswapMVar,但如果看一下该代码,您会发现情况截然不同-一个线程只需要读取MVar的当前值(例如,读取wHeld以查看是否按下了“ W”),并且需要另一个线程来无条件地写入新值(例如,写入{{ 1}}以更新“ W”键的当前状态)。不需要锁定,因为您只有两个线程,一个线程始终在写,而一个线程总是在读取最新写的内容。

wHeld second 问题是我认为您想返回最终寿命值,但是您返回的是damageCalculation调用的结果减去损坏之前的 old 生命值。换句话说,您可能希望swapMVar作为do-block的最后一行。

第三问题是您使用return newLife的方式。当您写时:

StdGen

使用let (randDano, newGen) = randomR (15,30) gen :: (Int, StdGen) 的值创建一个随机的gen值,然后返回生成器randDano的更新值。如果您丢掉此newGen并尝试再次使用newGen,您将始终生成相同的gen值。您可以使randDano成为newGen返回值的一部分,并将其用于下一次调用,也可以使生成器本身成为由damageCalculation函数更新的MVar。由于您想练习使用MVar,因此后者似乎是必经之路。因此,您的damageCalculation函数应如下所示:

damageCalculation

请注意damageCalculation :: MVar StdGen -> MVar Int -> IO Int damageCalculation v_gen v_life = do gen <- takeMVar v_gen let (randDano, gen') = randomR (15,30) gen putMVar v_gen gen' life <- takeMVar v_life let life' = life - randDano putMVar v_life life' return life' /修改值/ takeMVar模式的使用。

putMVar函数中,您可能希望在分叉之前先创建所有相关的MVar,例如:

main

对于损害计算,如果您要继续造成损害,直到寿命耗尽,请像这样定义一个循环函数,然后main :: IO () main = do putStrLn "Welcome to Hunter of Monsters" putStrLn "Whats your name?" l <- getLine putStrLn "Character" putStrLn ("Name: " ++ l) gen <- getStdGen v_gen <- newMVar gen putStrLn $ "Life Points: " ++ show lifePoints v_life <- newMVar lifePoints done <- newEmptyMVar putStrLn "The hunt begins" -- damage stuff here -- takeMVar done putStrLn "Game over!" 对其进行循环

forkIO

完整的程序在下面的清单2中。

清单1:让程序进行类型检查

let doDamage = do
    putStrLn "Damage calculation"
    rest <- damageCalculation v_gen v_life
    putStrLn $ " " ++ show rest
    if rest > 0
        then doDamage
        else putMVar done ()
forkIO doDamage

清单2:带有损坏循环的最终程序

import Control.Concurrent
import System.Random

lifePoints :: Int    -- use Int throughout
lifePoints = 100

damageCalculation :: StdGen -> MVar Int -> IO Int   -- use Int throughout
damageCalculation gen life = do
    let (randDano, newGen) = randomR (15,30) gen :: (Int, StdGen)
    a <- readMVar life
    let newLife = a - randDano   -- Use "a" (the value), not "life" (the MVar)
    a <- swapMVar life newLife   -- "swapMVar" needs an MVar ("life"), not just a value
    return a

main :: IO ()
main = do
    putStr "Welcome to Hunter of Monsters\n"
    putStr "Whats your name? \n"
    l <- getLine
    putStr "Character\n"
    putStrLn ("Name: " ++ l)
    let life = lifePoints
    putStrLn $ "Life Points: " ++ show life

    putStr "The hunt begins\n"
    a <- newEmptyMVar
    forkIO $ do
        putStr "Damage calculation\n"
        lifeMVar <- newMVar life
        gen <- getStdGen
        rest <- damageCalculation gen lifeMVar   -- IO action, so use '<-' not 'let'
        putStrLn $ " " ++ show rest
        putMVar a ()
    takeMVar a
    putStrLn "Game over!"