💬

[UEFN][Verse]配列のシャッフル[1]「乱数を重複無しで生成し、配列に格納する」アルゴリズムをVerselangで書いてみる

2023/09/29に公開

公式フォーラムで日本語で質問している方がいて、その内容が興味深かったので考えてみました。
https://forums.unrealengine.com/t/random/1308789

スレッドの主旨は「乱数を重複無しで生成し、配列に格納する」という物です。これは例えば「52枚のトランプからランダムに3枚選ぶ」というような処理で必要になる考え方です。

わかりやすさの為に、ここでは「1から52まで書かれたカードから任意枚数を選びかつ並び順をランダムにする」という処理をイメージします。「ランダムに複数の要素を選択し、かつ、同じ要素は選択しない」という点がポイントです。

投稿者さんは以下の様なJavaScriptコードを挙げ、これをVerseにコンバートするにはどうやるのかを尋ねていました。

function randomizing(){
   var arr = [];
   numArr = []; 

   //1
   for(var i=0; i < 5; i++){
      arr[i]=i+1;
   }

   //2
   for(var j = 0, var len = arr.length; j < 5; j++, len--) {
      //3
      rndNum = Math.floor(Math.random()*len);
      numArr.push(arr[rndNum]);
      //4
      arr[rndNum] = arr[len-1];
   }
}

アルゴリズムについて不勉強なこともあり、最初コードがしている処理の意味がわからりませんでした。折角なので多少詳しく解説しておきます。なお、サンプルコードな事もあり、この実装だと関数が値を返しませんが、その辺はニュアンスで対処しましょう。

//1 元の値の集合を連番で生成しています。具体的にはarr配列には[1,2,3,4,5]が格納されています。例えばここでi<52としておけばarr配列には1~52の値が格納されます。

//2 出力となるnumArr配列を生成するforループです。j < 5がnumArr配列の要素数を定義していて、ここでは要素数5の配列が生成されます。
lenは、arr配列内でアクセスを許す末端のインデックスを示します。ループ開始時は配列の終端を示し、ループのたびにデクリメントされます。このlenの挙動がポイントです。

//3 乱数を生成してarr配列から値を取得し、出力となるrndNum配列の末尾に追加します。生成する乱数は0からlenまでで、ループ開始時にはarr配列全体が取得の対象範囲となり、以降イテレーション毎に末尾から1要素ずつ減っていきます。

//4 //3で参照した配列要素はもう不要なので、arr配列でアクセスが許される範囲の末端要素の値をそこにコピーします。重なってるカードから1枚引き抜き、一番後ろにあるカードを抜いた場所に移動しているイメージです。
コード上ではコピー元が残っていますが、次のループでは(lenがデクリメントされるため)そこにはもうアクセスされません。このような仕組みで、同じ要素を選択しないようにしているのです。

Verseへのコンバート

では、このコードをVerseにコンバートしてみましょう。
Verseの流儀に合わせて変数名などを刷新していますが、出来るだけ元コードのアルゴリズムを踏襲しているつもりです。

Randomizing(NumberOfSheets:int, Hands:int ):[]int =
    var Deck :[]int= for:
        CardNumber:=1..NumberOfSheets
    do:
        CardNumber

    for:
        Number := 1..Hands
        PickTerminator := Deck.Length - Number
        RandomPick := GetRandomInt(0, PickTerminator)
        Result := Deck[RandomPick]
        set Deck[RandomPick] = Deck[PickTerminator]
    do:
        Result

#output
for:
    Index->Result:Randomizing(52, 5)
do:
    Print("{Index} : {Result}")

Randomizingはint配列を返します。このint配列はFor式が生成します。VerseのFor式は、各イテレーションごとに最後に評価した値からなる配列を返します。JavaScript版と随分見た目が異なりますが、振る舞いは同じです。

続く(かもしれない)

本当はこの後に配列シャッフル(フィッシャー・イェーツのシャッフル)に特化したVerseコードを書いてみるつもりだったんですが、いざやろうとしたら上手くいかず、早々に諦めてしまいました。

というわけで、配列シャッフルのコードの書き方が思いついたら続きます。関数型言語に向いた配列シャッフルアルゴリズムがあるのかな……?

続き

思いついたので書きました。
https://zenn.dev/t_tutiya/articles/284dfa5ebcc5c3

お知らせ

verse言語とUEFNの記事を他にも書いているので御覧下さい。
https://zenn.dev/t_tutiya

最後まで読んで頂きありがとうございました。この記事がお役に立てたようであれば、是非LIKEとフォローをお願いします(今後の執筆のモチベーションに繋がります)。

#Verse #UEFN #Fortnite #Verselang #UnrealEngine

宣伝

「Unityシェーダープログラミングの教科書」シリーズ1~5をBOOTHで頒布中です。
https://s-games.booth.pm/

Discussion