配列をランダムにシャフルしたい場合、有名なフィッシャーのコードがあります。
でも、人間がトランプ山などをシャッフルする場合、両手で物理的に行うので、フィッシャーのコードの様なランダム性が必ずしも出来ているわけではないですよね。
そんなわけで、人間がトランプ山などをシャッフルする場合の代表的な3つの方法を取り上げ、それを練習がてら javascript でコードを作ってみたいと思います。
※注1、各シャッフルの名前は、wikipedia を参考にしています。
※注2、今回のコードでは「カードの上下」を考慮しておりません。
※注3、各コードは、配列の 破壊的処理 を行っています。
※注4、各コードは、AI にも相談しましたが、完コピではありません。
ウォッシュシャッフル
これは、テーブルにカードを広げ、手でかき混ぜるシャッフルです。
特徴としては、
- かなりランダムに混ざりあう
- カードの上下もランダムになる (今回のコードでは未対応)
カードが程よく混ざり、ランダム性がかなり出てくるシャッフル方法なので、これは冒頭でも述べたフィッシャーのコードがあてはめられそうです。
const arr = [...Array(52).keys()];
console.log(arr);
const wash = (arr) => {
let i, n = arr.length;
while (n) {
i = Math.floor(Math.random() * n--);
[arr[i], arr[n]] = [arr[n], arr[i]];
}
return arr;
}
wash(arr);
console.log(arr);
結果
- [21, 31, 18, 1, 12, 24, 37, 29, 33, 19, 30, 38, 28, 50, 2, 35, 22, 14, 16, 8, 41, 11, 39, 44, 25, 47, 15, 26, 3, 6, 23, 42, 10, 5, 46, 49, 27, 36, 45, 20, 40, 9, 13, 51, 7, 43, 4, 32, 0, 48, 34, 17]
- [19, 18, 37, 21, 46, 35, 43, 34, 32, 29, 49, 5, 4, 0, 24, 20, 38, 27, 31, 26, 39, 40, 22, 15, 1, 28, 7, 3, 6, 13, 11, 9, 45, 48, 41, 25, 42, 30, 16, 17, 2, 10, 33, 36, 44, 12, 8, 47, 23, 50, 51, 14]
- [51, 30, 29, 26, 36, 28, 14, 21, 22, 32, 0, 13, 23, 15, 10, 16, 4, 46, 39, 5, 20, 41, 37, 9, 34, 19, 49, 25, 44, 7, 8, 50, 24, 40, 2, 38, 12, 27, 1, 31, 18, 33, 42, 3, 11, 45, 35, 17, 48, 43, 47, 6]
有名なコードを用いているので、特に問題はなさそうですね。
ヒンドゥーシャッフル
右手に持ったカード山の上側の数枚を、左手にどんどん移し替えていくシャッフルです。恐らく、日本人が一番なじみ深いシャッフルの一つなのではないでしょうか?
みんなよく「トランプを切って~」などと言う事も多いと思われるため、便宜上、関数名は cut(arr) としています。
const arr = [...Array(52).keys()];
console.log(arr);
const cut = (arr) => {
const temp = [];
while (arr.length) {
let n = Math.min(arr.length, Math.floor(Math.random() * 5) + 5);
temp.push(arr.splice(-n, n));
}
console.log(temp);
arr.push(...temp.flat());
return arr;
}
cut(arr);
console.log(arr);
結果
まずは、関数内の処理の途中の temp の値から。
[[43, 44, 45, 46, 47, 48, 49, 50, 51], [34, 35, 36, 37, 38, 39, 40, 41, 42], [28, 29, 30, 31, 32, 33], [22, 23, 24, 25, 26, 27], [14, 15, 16, 17, 18, 19, 20, 21], [6, 7, 8, 9, 10, 11, 12, 13], [0, 1, 2, 3, 4, 5]]
どうですか?各かたまりが、多次元配列で収まっているのがわかります。
あとはこれを flat() で成形などした形が次の通り。
[43, 44, 45, 46, 47, 48, 49, 50, 51, 34, 35, 36, 37, 38, 39, 40, 41, 42, 28, 29, 30, 31, 32, 33, 22, 23, 24, 25, 26, 27, 14, 15, 16, 17, 18, 19, 20, 21, 6, 7, 8, 9, 10, 11, 12, 13, 0, 1, 2, 3, 4, 5]
上手くいっているようですね。
尚、このコードはランダム性があるので、結果も毎回異なります。
- [44, 45, 46, 47, 48, 49, 50, 51, 35, 36, 37, 38, 39, 40, 41, 42, 43, 28, 29, 30, 31, 32, 33, 34, 19, 20, 21, 22, 23, 24, 25, 26, 27, 11, 12, 13, 14, 15, 16, 17, 18, 3, 4, 5, 6, 7, 8, 9, 10, 0, 1, 2]
- [47, 48, 49, 50, 51, 39, 40, 41, 42, 43, 44, 45, 46, 34, 35, 36, 37, 38, 25, 26, 27, 28, 29, 30, 31, 32, 33, 19, 20, 21, 22, 23, 24, 11, 12, 13, 14, 15, 16, 17, 18, 3, 4, 5, 6, 7, 8, 9, 10, 0, 1, 2]
- [46, 47, 48, 49, 50, 51, 41, 42, 43, 44, 45, 35, 36, 37, 38, 39, 40, 29, 30, 31, 32, 33, 34, 23, 24, 25, 26, 27, 28, 14, 15, 16, 17, 18, 19, 20, 21, 22, 9, 10, 11, 12, 13, 0, 1, 2, 3, 4, 5, 6, 7, 8]
解説
ランダムに5~10の枚数を出し、配列から切り出し、順番を変えながら別配列に収めていく。ざっとした流れはこんな感じです。
注意点としては、人間の動作をそのまま再現した場合、
- 右手山の先頭から切り出し -> 配列の先頭から
splice(0, n)など - 左手山の上に重ねていく -> 配列の先頭へ
unshift()など
こういったメソッドは、配列の先頭に要素を追加、削除が行われる度に 配列内の要素のインデックスを全て前後へズラしていかなければならない ため、要素数が多くなればなるほど莫大な時間が費やされてしまいます。
したがって、処理速度を重視しつつ、且つ、結果が同じになるように、配列の後ろ側をいじる処理にしてあります。具体的には、
- 右手山は下から切り出し ->
splice(-n, n)負インデックスで配列の後ろから切り出し - 左手山も下から追加 ->
push()で 配列末尾へ追加
いかがでしょうか?
もう少し良い処理があるような気もします。
もし、こんな方法があるよ~、という意見がありましたら、コメントお待ちしております。
リフルシャッフル
カード山を均等に二つに分け、両山からカードを交互に合わせていくシャッフルです。これは、プロマジシャンやカジノのディーラーなどが行うような、見た目テクニカルでカッコいいイメージのシャッフルです(笑)
二つの山を一つに合わせていく手法はいくつかあるようですが、両山の端と端を突き合わせ、両手の中でカードを「橋」の様にしならせながら シャフシャフシャフシャフ… っと合わせていく方法が、個人的にはテクニカルでカッコいい気がします。そんな関係から、便宜上、関数名は bridge(arr) としています。
精密 bridge(arr)
const arr = [...Array(52).keys()];
console.log(arr);
const bridge = (arr) => {
const mid = Math.floor(arr.length / 2);
const right = arr.splice(0, mid);
const left = arr.splice(0);
while(left.length || right.length) {
if(left.length) {
arr.push(left.pop());
}
if(right.length) {
arr.push(right.pop());
}
}
arr.reverse();
return arr;
}
bridge(arr);
console.log(arr);
結果
[0, 26, 1, 27, 2, 28, 3, 29, 4, 30, 5, 31, 6, 32, 7, 33, 8, 34, 9, 35, 10, 36, 11, 37, 12, 38, 13, 39, 14, 40, 15, 41, 16, 42, 17, 43, 18, 44, 19, 45, 20, 46, 21, 47, 22, 48, 23, 49, 24, 50, 25, 51]
きれいに交互に分かれました。
解説
まずカード山を二つに分け、それを交互に pop() させながらその戻り値を使い、同時に一つの山に push() 、最後に reverse() させています。この辺りは前述した通り、処理速度が遅くならないように、要素の追加と削除は配列の末尾から行っている結果となります。
考察
しかし、ここで一つの懸念が浮かび上がります。それは、 カードの枚数が同じなら、結果も常に同じになる ということです。これは言わば、プログラムの素直さが、奇しくもプロのテクニシャンの精密な技術を体現する形となったという事でしょう。
しかし、考えても見て下さい。プロマジシャンのようなテクニシャンならいざ知らず。もしこのシャッフルを素人がやったら、もう少しアバウトな感じになるのではないでしょうか? 精密bridge(arr) 、これはこれでもよいのかもしれませんが、もう少し素人っぽいっていうか?人間っぽい感じのシャッフルの需要もあるかもしれません。そこでもう少し人間っぽい感じを出したのが、次のコードです。
人間 bridge(arr)
const arr = [...Array(52).keys()];
console.log(arr);
const bridge = (arr) => {
const mid = Math.floor((arr.length / 2) + (Math.random() * 3 - 1));
const right = arr.splice(0, mid);
const left = arr.splice(0);
let n;
const temp = [];
const ratio = () => Math.random() < 0.90 ? 1 : 2;
while(left.length || right.length) {
if(left.length) {
n = Math.min(left.length, ratio());
temp.push(left.splice(-n, n));
}
if(right.length) {
n = Math.min(right.length, ratio());
temp.push(right.splice(-n, n));
}
}
console.log(temp);
arr.push(...temp.reverse().flat());
return arr;
}
bridge(arr);
console.log(arr);
解説
このコードは、上記の「精密 bridge(arr)」を基にしつつ、下記の様な人間っぽいアバウトな感じを追加しています。
- 二つの山は精密には分けず、両山に毎回 0~1枚の誤差を出す
- 両山を合わせる時、左右から1枚ずつを、たまには2枚出ちゃうようにする
結果
まずは、関数内の処理の途中の temp の値から。
[[51], [24], [50], [23], [49], [22], [48], [21], [47], [20], [46], [19], [45], [18], [44], [17], [43], [16], [42], [15], [41], [14], [40], [13], [39], [12], [38], [11], [37], [10], [36], [9], [35], [8], [34], [6, 7], [32, 33], [5], [31], [4], [30], [3], [29], [2], [28], [0, 1], [27], [25, 26]]
どうでしょう?所々、2枚続きでカードが出されている( [6, 7] や [0, 1] など)のが確認できると思います。それを最終的に整形した arr の値が次の通り。
[25, 26, 27, 0, 1, 28, 2, 29, 3, 30, 4, 31, 5, 32, 33, 6, 7, 34, 8, 35, 9, 36, 10, 37, 11, 38, 12, 39, 13, 40, 14, 41, 15, 42, 16, 43, 17, 44, 18, 45, 19, 46, 20, 47, 21, 48, 22, 49, 23, 50, 24, 51]
もちろん、素人のアバウトさがランダムで出てくるので、毎回結果は異なります。
- [26, 0, 27, 1, 2, 28, 3, 29, 4, 30, 31, 5, 32, 6, 33, 7, 8, 34, 9, 35, 10, 36, 11, 37, 12, 38, 13, 39, 14, 40, 15, 41, 16, 42, 17, 43, 18, 44, 19, 45, 20, 46, 21, 47, 22, 48, 49, 23, 24, 50, 25, 51]
- [27, 28, 29, 0, 30, 1, 31, 2, 32, 3, 4, 33, 5, 34, 6, 35, 7, 8, 36, 9, 37, 10, 38, 11, 39, 12, 40, 13, 41, 14, 42, 15, 43, 16, 44, 17, 18, 45, 19, 46, 20, 21, 47, 22, 23, 48, 24, 49, 25, 50, 26, 51]
- [0, 26, 1, 27, 2, 28, 3, 29, 4, 30, 5, 6, 31, 7, 32, 8, 33, 34, 9, 35, 10, 36, 11, 37, 12, 13, 38, 14, 39, 15, 40, 16, 41, 42, 17, 43, 18, 44, 19, 45, 20, 46, 21, 47, 22, 48, 23, 49, 24, 50, 25, 51]
まとめ
いかがだったでしょうか?
それ違うよ、コード改善できるよ、もっど他の良いヤツがあるよ
など有りましたら、コメントでお待ちしております。