このあいだ4次元美術館プレーヤーというのを書きました。美術館というのはパズルの名前で、もとは2次元上の盤面にやたら指向性の高い明かりを置いて、すべてのマスを照らすパズルです。こんな感じ。
で、何を思ったかこれを4次元に拡張したパズルを制作する人がいて、2次元でもデジタルの助けが欲しいパズルなのに4次元ともなるとさすがに紙とペンで解くには限界があり、それならということでプレーヤーを作りました。こんな感じです。なんかヤバそうですね。
って、そんな話はどうでもいいのでした。とにかく、4次元の盤面を生成したり操作したりするのには4次元配列が必要で、すなわち4重ループを回す必要に迫られたのです。
問題:深すぎるネスト
愚直に書くとまーこんな感じです。
for(let x = 0; x < boardX; x++){
for(let y = 0;y < boardY; y++){
for(let z = 0; z < boardZ; z++){
for(let w = 0; w < boardW; w++){
board[x][y][z][w] = hogehoge()
}
}
}
}
いやこれはさすがにダサい。筆者は別にfor文廃止論者ではありませんが、こんなん誰だって間違うしネストは深過ぎで見づらいし、いいことはなにもない。というわけで、解決策を考えましょう。
解決策①: 余りを用いる
なるべくネストは1重にしたいので、まずはそういうふうに書いてみます。
for(let index=0; index < boardX * boardY * boardZ * boardW; index++){
let x,y,z,w // どうやって求める?
}
index
からx,y,z,w
を求めるには、割り算の余りを使えそうです。
function calcQandR(n,m){
return [Math.floor(n/m), n%m]
}
function index2pos(i){
var [i,w] = calcQandR(i,boardW)
var [i,z] = calcQandR(i,boardZ)
var [x,y] = calcQandR(i,boardY)
return [x,y,z,w]
}
for(let index=0; index < boardX * boardY * boardZ * boardW; index++){
let [x,y,z,w] = index2pos(index)
//処理
}
行数は増えましたが、大分すっきりしました。ただ、index2pos
の中に定数が残っていたりして、まだ使い勝手が向上できそうです。
解決策②:ジェネレータを使う
ジェネレータについて詳しいことは他の記事を参照してもらうとして、ここではfor-ofで取り出し可能な座標リスト(正確には「イテラブルなオブジェクト」)を生成するものと思ってもらえれば十分です。
function* product(...sizes){
sizes = sizes.reverse()
let all = sizes.reduce((a,c) => a*c, 1)
for(let index=0; index<all; index++){
let result = [], i = index
sizes.forEach((size) => {
result.push(i%size)
i = Math.floor(i/size)
})
yield result.reverse()
}
}
for(let [x,y,z,w] of product(boardX,boardY,boardZ,boardW)){
//処理
}
すっきりしました。何次元でも対応できるように書いたので、5次元や6次元になっても最小限の書き換えで対応できます。やったぜ。
pythonでは
itertools
というパッケージがあり、その中のproduct
を使えば同じことが簡単に実現できます。jsでもこれが使えたらハッピーだねというだけの記事でした。