1

Im trying to learn MVar and i'm trying to make this text game that is:

The hunt begins at 100 life points and level 1 then

1.One function would do damage to life points in a range of (15,30) 2.Another function would heal life points in a range of (5,10) 3.Adds one level because the player survived 4.Repeats until life point reachs 0

Life points would be a MVar Integer so one function has to wait for the other to finish to change its value (synchronized mutable variable)

I want to repeat theses 2 functions until life points reaches 0 and then shows the level the player reached

This is what i got so far:

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

Im having trouble getting a new random number, just repeats one value. The damage function is simple but i get error trying to change the value of life. Also how can i repeat these functions until life points reachs 0? Does the player level needs to be a MVar as well?

  • 3
    Exactly why do you want to use an `MVar` here? It looks like a `State` is more appropriate? – Willem Van Onsem Jun 26 '19 at 14:15
  • `let rest = io action` does not run the action, use `rest <- io action` instead. Also reading and then swapping an `MVar` is not a thread-safe approach. Here you only have one, so it might be ok, but... – chi Jun 26 '19 at 14:18
  • @WillemVanOnsem i was thinking that damage function would change the value of lifepoints and then signal to healing function to start working. MVar would make so that one function waits for antother – rodrigo ferreira Jun 26 '19 at 14:27
  • 1
    Definitely don't read and swap the `MVar`; that doesn't make any sense. I would strongly suggest that you not fiddle with the `MVar` in `damageCalculation`; either make that pure or use `IO` in it to get the RNG. – dfeuer Jun 26 '19 at 14:29
  • @chi ok, but what about the other questions? – rodrigo ferreira Jun 26 '19 at 14:30
  • One question at a time. Thanks. :) –  Jun 26 '19 at 16:31

1 Answers1

1

There are a number of problems with your code before it'll even compile:

  • damageCalculation tries to use life as both an MVar Integer (i.e., a "token" for a storage location containing an Integer) and the Integer value itself
  • you've provided only one argument to swapMVar where it expects two
  • you've mixed Int and Integer types
  • in main, you've treated damageCalculation as a pure function instead of an I/O action

Fixing all of these bugs will allow the program to type-check (see Listing #1 below).

However, there are still a number bugs. First, the code:

a <- readMVar life
let newLife = a - randDano
a <- swapMVar life newLife

is subject to a race condition if multiple threads are trying to update the damage. The first line reads the value of the MVar but doesn't perform any kind of locking or synchronization, and the third line then unconditionally writes the new life (while grabbing a copy of the old value).

For example, if two threads try to deduct 10 points of damage each in the following, interleaved fashion, you'll run into a problem:

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

Instead, you want to use the pair of functions takeMVar and putMVar which are designed to provide automatic synchronization:

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

In my answer to your other question, I had recommended using readMVar and swapMVar, but if you look at that code, you can see that the situation was very different -- one thread only needed to read the current value of the MVar (e.g., reading wHeld to see if "W" was being pressed), and another thread needed to unconditionally write a new value (e.g., writing wHeld to update the current state of the "W" key). No locking was needed, because you just had two threads, one always writing, and one always reading whatever was most recently written.

The second problem with damageCalculation is that I think you want to return the final life value, but you're returning the result of the swapMVar call which will be the old life value before deducting damage. In other words, you probably want return newLife as the last line of your do-block.

The third problem is the way you're using your StdGen. When you write:

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

the value of gen is used to create a random randDano value, and then an updated value for the generator newGen is returned. If you toss this newGen away and try reusing gen again, you'll always generate the same randDano value. You can either make newGen part of the return value of damageCalculation and use it for the next call, or you can make the generator itself an MVar that gets updated by the damageCalculation function. Since you want practice using MVars, the latter seems like the way to go. So, your damageCalculation function should look like:

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'

Note the use of the takeMVar / modify value / putMVar pattern.

In the main function, you probably want to create all the relevant MVars up front before forking, so something like:

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

For the damage calculation, if you want to keep doing damage until the life points run out, define a looping function like so and then forkIO it:

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

The full program is in Listing #2 below.

Listing #1: Getting the program to type-check

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

Listing #2: Final program with a damage loop

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!"
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • You'd better force the value before installing it in the `MVar`. There's a function for modifying `MVar`s (strictly or not) that I believe takes care of exceptions too. – dfeuer Jun 27 '19 at 20:50