格子と二重バッファ
2次元の格子を、1次元の Uint8Array を1本だけ用意して y * N + x で住所を引いて持つ。整数しか入らない箱なので、状態が整数だという前提が型に出る。下は N * N の箱を市松模様で埋めて、状態 1 のマスを濃く、0 を薄く写したもの。
grid[y * N + x] で1本の配列を平面のように読み書きしている。(i / N) | 0 が行番号で、行と列の和の偶奇が市松の濃淡を分ける。
隣を引くと端をまたぐので、住所を取り出す関数に巻きを仕込む。(y + N) % N で、はみ出した座標を反対側へ折り返す。
// x,y がマス目の外でも、反対の縁から読み直す const idx = (g, x, y) => g[((y + N) % N) * N + ((x + N) % N)]
更新のルールには、中身を試すための雑な規則を置く。上下左右の隣の合計が奇数なら自分を反転、偶数ならそのまま。市松模様が点滅するだけになるが、近傍の和から次の状態を決める骨格は本物のCAと同じ。
// center: 自分の状態, sum: 上下左右の和 const rule = (center, sum) => (sum % 2 === 1 ? 1 - center : center)
引っかかったのが更新の順番。配列を1つしか持たずにその場で上書きしていくと、まだ更新していないマスが、もう更新済みの隣を見てしまう。左から右へ舐めると、左の更新結果が右の計算に漏れる。下は配列1枚を上書きしていく版。市松から始めたのに、更新が左上から斜めに伝染して模様が崩れる。
崩れの伝わる向きが、二重ループの走査順とそろう。左上から右下へ舐めた順に値が漏れる。読む面と書く面を分けると消える。grid を見ながら next に書いて、1世代終えたら2枚を入れ替える。下は上書き版(左)とバッファ版(右)を並べたもの。
格子CAは全マスを同じ瞬間の盤面から一斉に進める同期更新を建前にする。配列1枚をその場で上書きすると、走査の途中で書いた値を後ろのマスが隣として読み、同期のつもりが逐次更新(更新順に依存する系)にすり替わる。読む面(現世代)と書く面(次世代)を別の配列に分け、1世代ごとに参照を入れ替えるのがダブルバッファ。次世代の値はすべて現世代だけから決まるので、走査順に結果が左右されない。連続値の反応拡散まで、局所ルールを差し替える系がこの二枚使いに乗る。
同じルールなのに、読む面と書く面を分けたかどうかだけで右はきれいに点滅し、左は崩れる。