トランプなどのカードゲームは、山を混ぜてゲームに興じる事がほとんどである。JavaScriptでは、そのカードデータを配列に格納しておくことも多いと思われるが、「山を混ぜる」この部分の表現では、「配列をシャッフルするコード」が、必要不可欠である。
この配列シャッフルにおいては、先人の方々の研鑽により、様々な手法が開発されコードが公開されている。どのコードもそうなのだが、やはりコンピュータの仕事、その作業はアッいう間である(笑)しかしながら、アナログな人間の立場からすれば、もう少しゆっくりとその作業工程を確認したいではないか。
そこで当記事では、有名な配列のシャッフルコードの1つを用い、その工程をゆっくりと回し、人間にも簡単に視覚的に確認出来るようにしようと試みるものである。筆者はプログラム初心者であるがゆえ、公開コードは非常に分かりづらくなってしまった。稚拙ではあるが、開発時の方針や考え方が、皆さまの一助になれば、幸いである。
配列要素シャッフルコード
function shuffle(arr) { let n = arr.length; let temp, i; while (n) { i = Math.floor(Math.random() * n--); temp = arr[n]; //①:逃す arr[n] = arr[i]; //②:移動させる arr[i] = temp; //③:戻す } return arr; }
基本的には、
- 最終項(n)をtempへ逃し
- ランダム(i)を最終項(n)へ移動させ
- tempをランダム枠(i)へ戻す
この3工程を、n回分(arr.lemgth回分)繰り返す
それにしても、極限まで無駄をそぎ落とした、機能美とは正にこの事と言わんばかりのこの美しいフォルム…
黄金比率に近く見えるのは、気のせいだろうか(笑)
forEachでループをゆっくり回すコード
// 関数の配列をゆっくり実行する function loopSlowly2(funcArray, interval) { funcArray.forEach((func, i) => { setTimeout(func, i * interval); }); } // 処理したい関数を配列化する const funcArray = [ () => { console.log(1); }, () => { console.log(2); }, () => { console.log(3); }, () => { console.log(4); }, () => { console.log(5); } ]; // 1秒ごとに関数を順次実行する loopSlowly2(funcArray, 1000);
↓参考サイトと、↑サイト内のコードを引用
配列の各要素を順番にこなしていくforEach。そこにsetTimeoutをかませることで、時間差のある非同期処理を同時進行させてるわけね。そしてこの記事を読んで何よりも気づかされたこと、それは、「配列にラムダ式(アロー関数)を入れられる事」へぇ~、勉強になるなあ…
つまり、上記のシャッフルコードにおける「1.逃す、2.移動させる、3.戻す」の作業工程毎に作成したラムダ式を配列にpushしていき、あとからforEachで順番にこなしていけばいいという事になりますね。
sandboxによるサンプルコード
コードの進行と変数の値
さて、上記サンプルコードの進行のしかたであるが、
- シャッフルの部分のループが回る
- 同時に、各工程のラムダ式を、配列にpushしていく
- シャッフル部のループが終了
- forEachによる配列のスロー再生が開始する
大体はこんな感じで進行する。ここで注意しておきたいのは、「配列のスロー再生が始まるのは、シャッフルのループが終わってから」という点。つまり、配列内のラムダ式に変数がある場合、シャッフルのループが回り終わった、言わば変数の最終値を参照することになってしまうのだ。
変数の履歴を残す(重要)
そうならないように設けておく仕掛け、それが「変数の履歴を残しておく」ということ。ループの周回毎に合わせた変数の履歴があることで、後進行のforEadhが上手く振る舞えるのだ。例えるなら、
- 変数の履歴=進行台本
- 後進のラムダ式軍=台本通りに振る舞う役者
さらに付け加えるなら、「今は台本のシーンナンバー〇〇を演じてね」という、台本と役者とをマッチングさせる管理といった事も必要になりますね。
setTimeoutの関数に引数を使う方法
さて、上述の「台本と役者をマッチングさせる管理」ですが、下記のコードをご覧ください。
function display2(funcArray, interval) { funcArray.forEach((func, nnn) => { setTimeout(func, nnn * interval); }); }
forEachで配列要素から取り出された各func(中身は関数)が、setTimeoutの(関数,秒)の関数に収まり、作動する仕組み。台本とマッチングさせるためには、その関数が配列内の何番目か?が重要になるわけだが、せっかくforEachを使ってるわけだし、コールバックを上手く使ってやりたい。どういうことかというと、「forEachで、配列のインデックスがコールバック(nnn)で返ってくるので、その数値自体を、配列内の各関数の引数として使いたいよね」って事。しかしながら、
setTimeout(func, nnn * interval);を、
setTimeout(func(nnn), nnn * interval);
としたところで、上手く作動しない。なぜなら、setTimeoutに使う関数名に()を付けてしまうと、その関数は即時発動されてしまうから。
何か方法はないかと色々探したところ、うまいやり方があったので、ご紹介しておきます。
//これだと関数が即時発動されてしまう setTimeout(func(引数), α); //(引数)付きの関数を、もう一つの関数で外側から挟み込むと上手く行く setTimeout(() => {func(引数)}, α); //最終的には、この形 function display2(funcArray, interval) { funcArray.forEach((func, nnn) => { setTimeout(() => {func(nnn)}, nnn * interval); }); }
参考サイト↓
とても上手く行きました。
おわりに
いかがだったでしょうか?
こういった「ループをゆっくり回す方法」は、色々な手法があるのだと思われますが、今回は、配列でforEachを使っていく方法を試してみました。
もっとほかに効率の良いコードがあるかもしれませんが、その時はまた、サンプルコードの作成を挑戦してみようと思います。
それでは!
コメント