【问题标题】:Is there a way to make this text game in haskell using MVar?有没有办法使用 MVar 在 haskell 中制作这个文本游戏?
【发布时间】:2019-06-27 23:43:18
【问题描述】:

我正在尝试学习 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 吗?

【问题讨论】:

  • 究竟为什么要在这里使用MVar?看起来State 更合适?
  • let rest = io action 不运行该操作,请改用rest &lt;- io action。同样读取然后交换MVar 不是线程安全的方法。这里你只有一个,所以可能没问题,但是......
  • @WillemVanOnsem 我在想伤害功能会改变生命值的值,然后向治疗功能发出信号以开始工作。 MVar 将使一个函数等待另一个函数
  • 绝对不要阅读和交换MVar;这没有任何意义。我强烈建议您不要摆弄damageCalculation 中的MVar;要么把它变成纯的,要么在其中使用IO 来获取RNG。
  • @chi 好的,但是其他问题呢?

标签: multithreading haskell


【解决方案1】:

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

  • damageCalculation 尝试将 life 用作 MVar Integer(即包含整数的存储位置的“令牌”)和 Integer 值本身
  • 您只向 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”),并且另一个 线程需要无条件写入一个新值(例如,写入wHeld 以更新当前状态“W”键)。不需要锁定,因为您只有两个线程,一个始终在写入,一个始终读取最近写入的内容。

damageCalculation第二个 问题是我认为您想返回最终生命值,但您返回的是 swapMVar 调用的结果,这将是 扣除伤害前的旧寿命值。换句话说,您可能希望 return newLife 作为您的 do-block 的最后一行。

第三个问题是您使用StdGen 的方式。当你写:

let (randDano, newGen) = randomR (15,30) gen :: (Int, StdGen)

gen 的值用于创建随机的randDano 值,然后返回生成器newGen 的更新值。如果您将这个newGen 扔掉并尝试再次重用gen,您将始终生成相同的randDano 值。您可以将newGen 作为damageCalculation 的返回值的一部分并将其用于下一次调用,或者您可以将生成器本身设置为由damageCalculation 函数更新的MVar。由于您想练习使用 MVar,因此后者似乎是可行的方法。因此,您的 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模式的使用。

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

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它:

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 中。

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

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!"

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

import Control.Concurrent
import System.Random

lifePoints :: Int
lifePoints = 100

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'

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"

    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

    takeMVar done
    putStrLn "Game over!"

【讨论】:

  • 您最好在将其安装到MVar 之前强制该值。我相信有一个用于修改MVars 的功能(无论是否严格),它也可以处理异常。
猜你喜欢
  • 2011-10-26
  • 1970-01-01
  • 2022-12-03
  • 1970-01-01
  • 2021-06-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-21
相关资源
最近更新 更多