Еще один вопрос о случайных числах в Haskell

Я пытаюсь сделать версию игры Voltorb от Pokemon Gold и Silver на Haskell. Теперь для создания доски я хочу иметь список троек (l,r,v), где l — строка, r — строка, а v — значение поля.

Значения l и r реализованы с пониманием списка, поскольку они должны быть одинаковыми каждый раз. Что касается v, то я не могу найти вариант реализовать его так, чтобы он был 0,1,2 или 3 "случайным образом" (я знаю, что Haskell чисто функционален и в нем нет истинной случайности, это одна из причин, почему я бороться с этим).

Если кто-то может помочь с этим, я был бы очень благодарен. Если бы вы также могли кратко объяснить, почему это решение работает, это мне очень помогло бы.

Моя текущая реализация l и r:

field n = [(l,r,v) | l <- [0..n], r <- [0..n]]


person Shini    schedule 22.02.2020    source источник
comment
Учитывая, что есть много вопросов о случайных числах, в чем заключалась ваша проблема с адаптацией ответа? Что помешало вам создать список рандомов vs и определить field n vs = [ ... , v <- vs]?   -  person Thomas M. DuBuisson    schedule 22.02.2020
comment
@ThomasM.DuBuisson Я думаю, что вместо того, чтобы сжимать список случайных чисел в координаты сетки, это сгенерирует декартово произведение координат и случайных чисел, что приведет к значениям rows * cols * vs вместо желаемых значений rows * cols.   -  person MikaelF    schedule 23.02.2020


Ответы (2)


Если я правильно понимаю вопрос, должно быть одно случайное значение для каждой (Int, Int) позиции на доске. Таким образом, проблема не может быть решена путем добавления третьего пункта в список понимания, например:

field n = [(l,r,v) | l <- [0..n], r <- [0..n], v <- someRandomStuff]

поскольку длина выражения field тогда будет (n+1)x(n+1)x(длина случайного материала), а вам нужно просто (n+1)x(n+1).

Возможность состоит в том, чтобы действовать в два этапа:

  1. генерация необходимых (n+1)*(n+1) случайных значений от 0 до 3
  2. комбинируя это со значениями (l,r)

Я предполагаю, что читатель понимает генерацию псевдослучайных чисел из императивных языков.

Получив начальное число, вы можете использовать одноразовый генератор случайных чисел, возвращаемый функцией mkStdGen, для генерации случайных значений с помощью функции randomRs. Давайте используем сеанс ghci в качестве испытательного стенда.

Что касается шага 1:

 λ> import System.Random
 λ> :t randomRs
randomRs :: (Random a, RandomGen g) => (a, a) -> g -> [a]
 λ> 
 λ> seed1=42
 λ> 
 λ> getVSeq n seed = let rng0 = mkStdGen seed  in take  ((n+1)^2) (randomRs (0,3) rng0)
 λ> 
 λ> getVSeq 5 seed1
[1,1,3,0,2,1,0,1,0,1,3,1,2,0,2,3,1,1,3,2,0,2,2,0,2,0,0,0,1,0,2,1,0,2,0,1]
 λ> 
 λ> length $ getVSeq 5 seed1
36
 λ> field0 n = [(l,r) | l <- [0..n], r <- [0..n]]
 λ> field0 5
[(0,0),(0,1),(0,2),(0,3),(0,4),(0,5),(1,0),(1,1),(1,2),(1,3),(1,4),(1,5),(2,0),(2,1),(2,2),(2,3),(2,4),(2,5),(3,0),(3,1),(3,2),(3,3),(3,4),(3,5),(4,0),(4,1),(4,2),(4,3),(4,4),(4,5),(5,0),(5,1),(5,2),(5,3),(5,4),(5,5)]
 λ> 

 λ> 
 λ> 
 λ> length  $ field0 5
36
 λ> 

Теперь, что касается шага 2, функция zip почти решает нашу проблему, за исключением того, что мы не получаем точно триплеты:

 λ> 
 λ> sol0 n seed = zip (field0 n) (getVSeq n seed)
 λ> sol0 5 seed1
[((0,0),1),((0,1),1),((0,2),3),((0,3),0),((0,4),2),((0,5),1),((1,0),0),((1,1),1),((1,2),0),((1,3),1),((1,4),3),((1,5),1),((2,0),2),((2,1),0),((2,2),2),((2,3),3),((2,4),1),((2,5),1),((3,0),3),((3,1),2),((3,2),0),((3,3),2),((3,4),2),((3,5),0),((4,0),2),((4,1),0),((4,2),0),((4,3),0),((4,4),1),((4,5),0),((5,0),2),((5,1),1),((5,2),0),((5,3),2),((5,4),0),((5,5),1)]
 λ> 

Итак, нам нужно немного помассировать результат sol0:

 λ> 
 λ> sol1 n seed = let flatten = (\((a,b),c) -> (a,b,c))  in  map flatten (sol0 n seed)
 λ> sol1 5 seed1
[(0,0,1),(0,1,1),(0,2,3),(0,3,0),(0,4,2),(0,5,1),(1,0,0),(1,1,1),(1,2,0),(1,3,1),(1,4,3),(1,5,1),(2,0,2),(2,1,0),(2,2,2),(2,3,3),(2,4,1),(2,5,1),(3,0,3),(3,1,2),(3,2,0),(3,3,2),(3,4,2),(3,5,0),(4,0,2),(4,1,0),(4,2,0),(4,3,0),(4,4,1),(4,5,0),(5,0,2),(5,1,1),(5,2,0),(5,3,2),(5,4,0),(5,5,1)]
 λ> 

Я так понял, это то, что ты хотел. Если это единственное использование случайных чисел в вашем приложении, этого может быть достаточно. В противном случае, я боюсь, необходимо получить некоторые знания о генерации случайных чисел в контексте функционального программирования Haskell. Вы можете начать здесь или здесь.

Кроме того, как упоминал Томас М. ДюБюиссон, это было рассмотрено в нескольких вопросах SO. Вы можете использовать местную поисковую систему. Вот, например, один из последних.

Что делать, если вам нужно вернуть генератор для повторного использования?

В этом случае вам нужна функция, которая принимает предварительно созданный генератор и возвращает ОБА список триплетов «поля» и генератор (конечное состояние) в виде пары (список, finalRng).

Вы можете поручить тяжелую (случайную) работу функции, которая возвращает другую, более простую пару, состоящую только из списка значений v и конечного состояния генератора. Эта функция может быть написана рекурсивно, как показано ниже.


import  System.Random
import  Control.Monad.Random

-- returns the random v values AND the final state of the generator
seqAndGen :: RandomGen tg => (Int,Int) -> Int-> tg -> ([Int], tg)
seqAndGen range count rng0 =
    if (count <= 0)
        then ([],rng0)
        else
            let (v,rng1) = randomR range rng0
                nextSeq  = seqAndGen range (count-1) rng1  -- recursive call
            in
                (v:(fst nextSeq), snd nextSeq)

-- returns the "field" values AND the final state of the generator
fieldAndGen :: RandomGen tg => Int -> tg -> ([(Int,Int,Int)], tg)
fieldAndGen n rng0 =
    let  field0  = [(l,r) | l <- [0..n], r <- [0..n]]
         range   = (0,3)       -- at that level, range gets hardwired
         count   = (n+1)*(n+1) -- number of field/board positions
         pair    = seqAndGen range count rng0  -- the hard work
         vSeq    = fst pair
         endRng  = snd pair
         flatten = \((a,b),c) -> (a,b,c)
         field   = map flatten  (zip field0 vSeq)
    in
         (field, endRng)

Основная программа:

main = do
    let mySeed = 42
        n      = 5
    putStrLn $ "seed=" ++ (show mySeed) ++ "  " ++ "n=" ++ (show n)
    -- get a random number generator:
    let rng0    = mkStdGen mySeed  

    let (field, endRng) = fieldAndGen n rng0
        fieldv = map  (\(a,b,c) -> c)  field
    putStrLn $ "endRng = " ++ (show endRng)
    putStrLn $ "field  = " ++ (show field)

Вывод программы:


seed=42  n=5
endRng = 1388741923 1700779863
field  = [(0,0,1),(0,1,1),(0,2,3),(0,3,0),(0,4,2),(0,5,1),(1,0,0),(1,1,1),(1,2,0),(1,3,1),(1,4,3),(1,5,1),(2,0,2),(2,1,0),(2,2,2),(2,3,3),(2,4,1),(2,5,1),(3,0,3),(3,1,2),(3,2,0),(3,3,2),(3,4,2),(3,5,0),(4,0,2),(4,1,0),(4,2,0),(4,3,0),(4,4,1),(4,5,0),(5,0,2),(5,1,1),(5,2,0),(5,3,2),(5,4,0),(5,5,1)]

Обратите внимание, что возможен вариант, когда вместо обхода генератора вы передаете бесконечный список значений v, сгенерированный функцией randomRs. Удобно использовать функцию splitAt для такой цели. Но это предполагает, что вы используете случайность только для значений v и ничего больше, так что это немного менее общее и менее гибкое.

person jpmarinier    schedule 22.02.2020
comment
Большое спасибо, это именно то, что мне было нужно, и мне очень помогло. Можно ли получить список бесконечной длины вместо заданной длины? Затем я мог бы просто удалить первые записи для каждого нового поля, чтобы игроку не приходилось вручную вводить семя в каждой новой игре, а вводить его при запуске программы. - person Shini; 23.02.2020
comment
@Shini Но на самом деле randomRs возвращает бесконечный список. Я думаю, что на самом деле лучше также получить конечное состояние генератора из функции, чтобы вы могли повторно использовать его для своего следующего игрока. Я добавил код для этого в конце своего ответа. Надеюсь, поможет. - person jpmarinier; 24.02.2020

Вот краткий способ отделить (1) псевдослучайный чистый код от (2) случайного заполнения генератора псевдослучайных чисел:

--Get the generator in IO monad
main :: IO ()
main = do
  g <- getStdGen
  print $ buildGrid g 5

--Keep as much code as possible pure
buildGrid :: StdGen -> Int -> [(Int, Int, Int)]
buildGrid g n = zipWith ($) ((,,) <$> [0..n] <*> [0..n])
                            (take ((n+1) * (n+1)) $ randomRs (0,3) g)

Или вы можете сохранить исходное понимание списка, но в этом случае вам нужно использовать расширение языка:

{-# LANGUAGE TupleSections #-}
...
buildGrid g n = zipWith ($) [(y,x,) | y <- [0..n], x <- [0..n]] 
                            (take ((n + 1) * (n + 1)) $ randomRs (0,3) g) 
person MikaelF    schedule 23.02.2020
comment
(,,) <$> [0..n] <*> [0..n] = liftA2 (,,) [0..n] [0..n] может быть понятнее. :) - person Will Ness; 24.02.2020