目次/第02章
同じ種からは、同じ料理ができる
seeded pseudo random — シード付き擬似乱数
- 全14本とも
Math.random()を使っていません。代わりに自分で乱数を書いています。 - 「種(シード)」という整数をひとつ渡すと、何度実行しても同じ並びの数が出てきます。
- 同じ形が二度と作れないと、直すことも、比べることもできないからです。
ウィンナーを8本焼くとします。8本とも同じ太さ・同じ曲がり・同じ焼きムラなら、 それは工場の製品ではなくコピー&ペーストに見えます。だから乱数で揺らします。 ここまではふつうです。問題はその次で、ふつうの乱数だと二度と同じ8本が作れません。
3本目だけ焼き色が変で直したい。ところが Run するたびに全部変わるので、
直したのか、たまたま別の個体が出ただけなのか分かりません。
だからこのプロジェクトは Math.random() を1回も使わず、
種を渡すと毎回同じ数列を返す乱数を自分で書いています。
// 1. Rng : シード付き擬似乱数(mulberry32) const Rng = (seed) => { let s = seed >>> 0; return () => { s = (s + 0x6D2B79F5) | 0; let t = Math.imul(s ^ (s >>> 15), 1 | s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; };
中身は「かき混ぜて、上の桁と下の桁をぶつけて、また混ぜる」だけです。 規則はあるのに、出てくる数の並びに規則が見えない。だから擬似乱数といいます。 大事なのは仕組みより性質のほうで、入れた種が同じなら、出てくる数列も同じ。 これが「同じ料理をもう一度作れる」ということです。
実験 — 種を変える / 種を固定したまま作り直す
ウィンナー8本の長さ・太さ・曲がり・焼きムラを、乱数から作っています。 「もう一度作る」を押したとき、Rng なら同じ8本、Math.random() なら毎回別の8本になります。
種は、1個ではなく何個も要る
実際のコードでは、種をひとつだけ持つのではなく用途ごとに分けています。 たこ焼き.js なら、生地の形の乱数・焼きムラの乱数・ソースの垂れの乱数が別々の種から出ます。 分けておかないと、「ソースの量を変えたら生地の形まで変わってしまった」ということが起きるからです。
// 個体ごとに種をずらす。同じ列でも i が違えば別の個体になる const rnd = Rng(seed + i * 1013); // 形 const rndB = Rng(seed + i * 1013 + 77); // 焼き色
種を足し算だけで作ると、ぶつかることがあります。
seed + i と seed + j + 1 のような作り方をすると、
別々のはずの個体がたまたま同じ種になり、双子が生まれます。
掛ける数を大きめの素数(1013 や 9176 など)にしておくのはそのためです。
正規分布のほうが「らしい」ことがある
rnd() は 0〜1 のあいだを平らに出します。長さを ±10% 振りたいときはこれでよいのですが、
「たいていは真ん中くらい、たまに極端」という揺れ方をさせたいときは、
平らな乱数だと極端なものが多すぎます。そこで ガウス(正規分布)を使います。
クッキーの反りや、団子の練りむらがこれです。
// Box-Muller 法。平らな乱数2つから、釣鐘型の乱数を作る const gauss = (m, sd) => m + sd * Math.sqrt(-2 * Math.log(Math.max(1e-9, rnd()))) * Math.cos(6.28318 * rnd());
- 上の実験で種を 7 から 8 に変え、8本すべてが変わることを確かめる。
- Math.random() に切り替えて「もう一度作る」を3回押し、毎回変わることを確かめる。
- ウィンナー.js を開いて Rng という語を検索し、何か所で新しい乱数を作っているか数える。
- 「同じ種なら同じ結果」でないと困る場面を、料理以外で3つ考えてみる。