我正在玩小型ASCII游戏中的程序生成,并且在haskell中遇到了带有随机数的问题。基本思想是提供一个随机数,该随机数以游戏世界中某个部分的(x,y)为种子,例如,确定那里是否有树(this guy explains it nicely)
这是我为每一代尝试不同种子时得到的:
randomFromSeed :: Int -> Int -> Int
randomFromSeed max seed = fst (randomR (0, max - 1) (mkStdGen seed))
Prelude> map (randomFromSeed 10) [1..20]
[5,9,3,7,1,5,9,3,7,1,5,9,3,7,1,5,9,3,7,1]
显然周期为5,但另一方面,mkStdGen docs上显示为:
函数mkStdGen通过将Int映射到生成器中,提供了一种生成初始生成器的替代方法。 同样,不同的论点可能会产生不同的生成器。
那怎么来了,看来只有5个不同的生成器来了?
当给予不同的种子时,如何才能使它们真正地随机?
修改 出于某些奇怪的原因,使用更大的数字会更好:
Prelude> let mult = 1000000 in map (randomFromSeed 10) [0,mult .. 20*mult]
[3,7,0,6,9,2,8,1,4,0,3,9,2,5,1,4,7,3,6,9,5]
答案 0 :(得分:2)
那么,为什么只有5个不同的生成器出现呢?
认为只有5个发电机是一种幻想。如果您打印每个序列的第二个数字而不是第一个,您将得到以下信息:
random2ndFromSeed :: Int -> Int -> Int
random2ndFromSeed max seed =
let g0 = mkStdGen seed
(v1, g1) = randomR (0, max - 1) g0
(v2, g2) = randomR (0, max - 1) g1
in v2
λ>
λ> map (random2ndFromSeed 10) [1..40]
[6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3]
λ>
所以周期性似乎是8而不是5!
摆脱明显问题的一种方法是将Threefish替换为标准生成器,该生成器具有较新的设计并且具有更好的统计属性。或者,您也可以使用Dave Compton提到的pcg-random。
import System.Random.TF
tfRandomFromSeed :: Int -> Int -> Int
tfRandomFromSeed max seed = let g0 = mkTFGen seed
in fst $ randomR (0, max - 1) g0
λ>
λ> map (tfRandomFromSeed 10) [1..40]
[4,5,6,7,5,3,3,0,0,4,2,8,0,4,1,0,0,1,3,5,6,4,3,6,4,0,3,6,4,0,2,4,5,9,7,3,8,5,2,4]
λ>
更一般地说,随机性的出现应该是由生成器next
函数的重复应用引起的。在此,该功能对于每个种子/序列仅应用一次,因此不会要求随机性。
根据评论,实际需要是2D空间中点的“随机”功能。如果玩家在经过一些随机游走之后返回到已经访问过的某个点,则有望找到与之前相同的随机值,而这不会记住先前的随机值。
要以某种方式使我们获得有关随机值统计特性的保证,我们需要使用一个种子和一个随机序列来完成;那就是我们的应用数学家是testing。
我们需要两件事来产生这样一个持久的二维随机字段:
例如,可以通过利用基本集理论中的Cantor Pairing Function来完成此操作。
我们可以使用以下代码:
-- limited to first quadrant, x >= 0 and y >= 0:
cantor1 :: Int -> Int -> Int
cantor1 x y = y + (let s = x + y in div (s * (s+1)) 2)
-- for all 4 quadrants:
cantor :: (Int, Int) -> Int
cantor (x,y) =
let quadrant
| x >= 0 && y >= 0 = 0
| x < 0 && y >= 0 = 1
| x < 0 && y < 0 = 2
| x >= 0 && y < 0 = 3
| otherwise = error "cantor: internal error #1"
cant1
| x >= 0 && y >= 0 = cantor1 x y
| x < 0 && y >= 0 = cantor1 (-1-x) y
| x < 0 && y < 0 = cantor1 (-1-x) (-1-y)
| x >= 0 && y < 0 = cantor1 x (-1-y)
| otherwise = error "cantor: internal error #2"
in
4*cant1 + quadrant
通过这一初步步骤,我们必须认识到常规的Haskell随机数生成API不太适合手头的任务。
API通过next函数提供对随机序列的顺序访问。但是没有任意访问权限,例如discard函数在C ++随机库中提供的访问权限。使用MonadRandom界面的经典monadic风格全都与顺序访问有关。基本上就像州立单子。
此外,对于一些随机数生成器,根本不可能有效访问序列的任意点。在这种情况下,C ++ discard
函数仅使用昂贵的单步操作即可到达所需位置。
幸运的是,有Haskell implementation是Pierre L'Ecuyer等人的MRG32k3a随机数生成器。
使用MRG32k3a,对随机序列的任意访问归结为2个Galois场中小矩阵的幂。感谢古老而受人尊敬的Indian exponentiation algorithm,这可以在O(log n)时间内完成。
github中的MRG32k3a代码未提供完整的Haskell样式接口,例如RandomGen
实例,因此我们必须在其周围添加一些包装器代码。
首先,我们需要一些导入子句:
import System.Random
import System.Random.TF
import qualified Data.List as L
import qualified Text.Printf as TP
import qualified Data.Text as TL
import qualified Data.ByteString as BS
import qualified Data.Text.Encoding as TSE
import qualified Crypto.Hash.SHA256 as SHA
import qualified System.Random.MRG32K3A.Simple as MRG
,然后是包装程序代码本身:
newtype MRGen = MRGen MRG.State -- wrapper type for MRG32k3a generator
deriving Show
instance RandomGen MRGen where
genRange = let mrg32k3a_m1 = ((2::Integer)^32 - 209)
in const (0::Int, fromIntegral (mrg32k3a_m1 - 1))
next (MRGen g0) = let (v, g1) = MRG.next g0
in ((fromIntegral v)::Int, MRGen g1)
split (MRGen g0) = let g1 = MRG.advance ((2::Integer)^96) g0
in (MRGen g0, MRGen g1)
mkMRGen :: Int -> MRGen
mkMRGen userSeed = let longSeed = hashSeed userSeed
g0 = MRG.seed longSeed
in MRGen g0
ranSeek :: MRGen -> Integer -> MRGen
ranSeek (MRGen g0) count = let g1 = (MRG.advance count g0) in MRGen g1
hashSeed :: Int -> Integer
hashSeed userSeed =
let str = "MRG32k3a:" ++ (TP.printf "0x%x" userSeed)
bytes = (TSE.encodeUtf8 . TL.pack) $ str
ints = (map (fromIntegral) $ BS.unpack (SHA.hash bytes)) :: [Integer]
in
L.foldl' (\acc d -> acc*256 + d) 0 (take 20 ints)
功能mkMRGen
与mkStdGen
类似。函数ranSeek :: MRGen -> Integer -> MRGen
在O(log n)时间提供对随机序列的任意访问。
旁注::我在mkMRGen
中哈希用户提供的种子。这是因为github包使用其种子作为随机序列的偏移量。因此,为了避免小用户种子出现序列重叠的风险,我需要从用户种子中生成大量种子。
由于我们的RandomGen
实例,我们可以使用常规功能,例如random :: RandomGen g => g -> (a, g)。例如,我们可以像这样从简单的Int
种子生成Double类型的2D随机字段:
randomDoubleField :: Int -> (Int, Int) -> Double
randomDoubleField userSeed (x,y) =
let k = 1 -- number of needed random values per plane point
g0 = mkMRGen userSeed
g1 = ranSeek g0 (fromIntegral (k * cantor (x,y)))
in fst (random g1)
现在我们有了这么小的工具包,我们可以编写一个小的测试程序,为零点邻域绘制一些随机景观,每个2D点一个字符。
说,字符“ t”代表一种类型的树,“ T”代表另一种类型的树。没有树时用减号表示。
randomCharField :: Int -> (Int, Int) -> Char
randomCharField userSeed (x,y) =
let n = floor (8.0 * randomDoubleField userSeed (x,y) )
in "------tT" !! n
rowString :: Int -> Int -> Int -> String
rowString userSeed size y =
let xRange = [(-size) .. size]
in map (randomCharField userSeed) [ (x,y) | x <- xRange ]
main = do
let userSeed = 42
size = 6
yRange = [(-size) .. size]
mapM_ (putStrLn . (rowString userSeed size)) yRange
--t-T----TT-t
------t-----T
-T--T--T-----
--t-T--tTTT--
--T--t---T---
t-Tt------t--
-T-----t-T---
-T-t-t----T--
tT-tT---tT--t
---TTt---t---
-------T---t-
--t---------t
-tT-t---t----
优化说明:
如果需要考虑性能,则可能需要将(mkMRGen userSeed)
计算移出循环。
答案 1 :(得分:1)
通过使用pcg-random而不是random可以避免看到的意外行为:
import System.Random.PCG
import Control.Monad.ST
randomFromSeed :: Int -> Int -> Int
randomFromSeed max seed = runST $ do
g <- initialize (fromIntegral seed) 0
uniformR (0, max - 1) g
main :: IO ()
main = print $ map (randomFromSeed 10) [1..20]