Reputation: 815
I need multiple random numbers in different ranges (which could be avoided by multiplying the result of the random number generator with a constant).
I'm making an Asteroid clone in Haskell, and I want to generate randomly spawned enemies. I want to let them spawn at the edge of the screen with the same velocity (so norm of velocity vector equal).
Additional info: When an entity hits the edge of the screen it appears again at the other side, this is why I chose to only let enemies spawn at two of four edges of the screen and made the velocity really determine where you first see them appear.
I looked at System.Random
and ran into difficulties. I figured I needed five random numbers:
At the start of the game I generate a new StdGen and after that I do this every frame.
Solutions I thought of but thought were bad practice: Instead of passing down generators through methods, create a new generator every time using newStdGen
.
I also thought of calling the newRand function for all the random numbers I need. But if I had a function that required two random numbers in the same range, the two random numbers would be identical, since identical input always gives identical output in Haskell.
Problem: Ambiguous type variables at all the random calls except for the call to the newRand function (that is also used to update the generator each frame), because Haskell doesn't know which type of number to use.
Sample error:
src\Controller.hs:45:56: error:
* Ambiguous type variable `a0' arising from the literal `0.0'
prevents the constraint `(Fractional a0)' from being solved.
Relevant bindings include
axis :: a0 (bound at src\Controller.hs:45:25)
Probable fix: use a type annotation to specify what `a0' should be.
These potential instances exist:
instance HasResolution a => Fractional (Fixed a)
-- Defined in `Data.Fixed'
instance Fractional Double -- Defined in `GHC.Float'
instance Fractional Float -- Defined in `GHC.Float'
...plus three instances involving out-of-scope types
(use -fprint-potential-instances to see them all)
* In the expression: 0.0
In the first argument of `randomR', namely `(0.0, 1.0)'
In the second argument of `($)', namely `randomR (0.0, 1.0) gen'
|
45 | axis = signum $ fst $ randomR (0.0, 1.0) gen
| ^^^
My code:
newRand :: StdGen -> (Float, Float) -> (Float, StdGen)
newRand gen (a, b) = randomR (a, b) gen
genEnemies :: StdGen -> Float -> [Moving Enemy]
genEnemies gen time | rand > 995 = [Moving (x, y) (vx, vy) defaultEnemy]
| otherwise = []
where rand = fst $ newRand (0, 1000) gen
x | axis < 0.5 = fst $ randomR (0.0, width) gen
| otherwise = 0
y | axis >= 0.5 = fst $ randomR (0.0, height) gen
| otherwise = 0
axis = signum $ fst $ randomR (0.0, 1.0) gen
vx = fst $ randomR (-20.0, 20.0) gen
vy = sgn * sqrt (400 - vx*vx)
sgn = (signum $ fst $ randomR (-1.0, 1.0) gen)
Upvotes: 1
Views: 219
Reputation: 153192
The usual pattern when you want to generate multiple random numbers in Haskell goes like this:
foo :: StdGen -> (StdGen, (Int, Int, Int))
foo g0 = let
(val1, g1) = randomR (0, 10) g0
(val2, g2) = randomR (0, 10) g1
(val3, g3) = randomR (0, 10) g2
in (g3, (val1, val2, val3))
For example, in ghci:
System.Random> foo (mkStdGen 0)
(1346387765 2103410263,(7,10,2))
You can see it returned three different numbers -- unlike what you would get if you called randomR
with g0
each time as you did in your code.
Hopefully you catch the pattern: call randomR
with the range you want and a StdGen
; use the value it returned as your random piece and the StdGen
it returned as the input next time you call randomR
. It's also important that you return the updated StdGen
from your random function, so that you can continue the pattern in later calls.
Later you can look into monads, specifically RandT
, which can abstract away the process of feeding the most recently updated StdGen
into the next call to randomR
. A sample of the RandT
style looks like this:
foo' :: MonadRandom m => m (Int, Int, Int)
foo' = do
val1 <- getRandomR (0, 10)
val2 <- getRandomR (0, 10)
val3 <- getRandomR (0, 10)
return (val1, val2, val3)
...but for now, stick with the basics. Once you understand them thoroughly it will feel much less magical when you implement (or reuse) the abstractions that let you do that kind of thing.
Upvotes: 4