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