10
\$\begingroup\$

For Prof. Yorgey's 2013 Haskell course, I'm working on a homework to simulate a Risk battle.

type Army = Int

data Battlefield = Battlefield { attackers :: Army, defenders :: Army } 
                    deriving (Show, Eq)

In addition, Prof. Yorgey implement the following code to simulate the random throwing of dice:

newtype DieValue = DV { unDV :: Int } 
  deriving (Eq, Ord, Show, Num)

first :: (a -> b) -> (a, c) -> (b, c)
first f (a, c) = (f a, c)

instance Random DieValue where
  random           = first DV . randomR (1,6)
  randomR (low,hi) = first DV . randomR (max 1 (unDV low), min 6 (unDV hi))

die :: Rand StdGen DieValue
die = getRandom

The second homework question asks to implement the battle function:

which simulates a single battle (as explained above) between two opposing armies. That is, it should simulate randomly rolling the appropriate number of dice, interpreting the results, and updating the two armies to reflect casualties. You may assume that each player will attack or defend with the maximum number of units they are allowed.

And then for my implementation:

battle :: Battlefield -> Rand StdGen Battlefield
battle bf = return $ battleOneRound bf

battleOneRound :: Battlefield -> Battlefield
battleOneRound bf = updateArmy bf (compete a_dice d_dice)
  where
    a_dice = rollDieN . getLegalAttackers $ (attackers bf)
    d_dice = rollDieN . getLegalDefenders $ (defenders bf)

rollDieN :: Army -> [DieValue]
rollDieN n 
  | n <= 0     = []
  | otherwise = evalRand die (mkStdGen (1000*n)) : rollDieN (n-1) 

type AttackersDice = [DieValue]
type DefendersDice = [DieValue]

-- defender must have at least 2 left at base
getLegalDefenders :: Army -> Army
getLegalDefenders n 
  | n >= 4    = 2
  | n == 3    = 1
  | otherwise = 0

-- attackers must have at least 1 left at base
getLegalAttackers :: Army -> Army
getLegalAttackers n 
  | n >= 4    = 3
  | n == 3    = 2
  | n == 2    = 1
  | otherwise = 0

Testing

ghci> let bf = battle $ Battlefield 10 20

ghci> (evalRand bf) $ mkStdGen 10
Battlefield {attackers = 8, defenders = 20}

ghci> (evalRand bf) $ mkStdGen 5
Battlefield {attackers = 8, defenders = 20}

ghci> (evalRand bf) $ mkStdGen 444444
Battlefield {attackers = 8, defenders = 20}

As you can see, the actual StdGen argument does not appear to have any impact on the battle results. That's because rollDieN uses mkStdGen (1000*n) to simulate random die throws.

Please critique this implementation. I'm not sure if I've achieved randomness in the battle function.

\$\endgroup\$
  • \$\begingroup\$ Just to clarify, your comment about StdGen not affecting the outcome, that is intended behavior, correct? \$\endgroup\$ – RubberDuck Jan 24 '15 at 3:01
  • \$\begingroup\$ Not necessarily. My only question is whether I've implemented the battle function in a way that aligns with the homework question. \$\endgroup\$ – Kevin Meredith Jan 24 '15 at 3:04
6
+100
\$\begingroup\$

rollDieN doesn't look very random to me

*Risk> map unDV $ rollDieN 100
[5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,
 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6]

This looks better:

*Risk> map unDV $ evalRand (replicateM 100 die) (mkStdGen 123456)
[4,5,2,3,5,6,2,3,2,5,4,5,5,5,1,1,6,6,2,3,2,5,5,3,4,1,3,1,
 3,2,4,4,3,2,2,6,2,2,3,1,6,6,4,5,3,6,6,6,5,2,2,1,6,3,6,3,
 6,3,6,5,6,5,1,1,1,2,1,5,4,4,3,3,3,3,3,3,4,5,5,2,4,1,5,4,
 4,2,1,1,5,1,4,2,6,5,3,5,2,3,6,1]

Now we can write a function that will return the two army's dice rolls

rollDice n = replicateM n die

battleTest :: Battlefield -> Rand StdGen ([Int], [Int])
battleTest battlefield = do
    attackerRolls <- rollDice $ getLegalAttackers (attackers battlefield)
    defenderRolls <- rollDice $ getLegalDefenders (defenders battlefield)
    return (map unDV attackerRolls, map unDV defenderRolls)

And check that our output looks sane

*Risk> evalRand (battleTest (Battlefield 10 20)) (mkStdGen 12)
([6,3,2],[5,1])
*Risk> evalRand (battleTest (Battlefield 10 20)) (mkStdGen 123456)
([4,5,2],[3,5])

Now assuming a function to update the battlefield based on dice rolls, we just change it to

battle :: Battlefield -> Rand StdGen Battlefield
battle battlefield = do
    attackerRolls <- rollDice $ getLegalAttackers (attackers battlefield)
    defenderRolls <- rollDice $ getLegalDefenders (defenders battlefield)
    return $ nextBattlefield attackerRolls defenderRolls battlefield

There's also a problem with getLegalDefenders. The homework says

The defending player may defend with up to two units (or only one if that is all they have).

So instead of

getLegalDefenders :: Army -> Army
getLegalDefenders n 
  | n >= 4    = 2
  | n == 3    = 1
  | otherwise = 0

It should look like

getLegalDefenders :: Army -> Army
getLegalDefenders n 
  | n >= 2    = 2
  | n == 1    = 1
  | otherwise = 0

Or

getLegalDefenders n = max 0 (min 2 n)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.