diff --git a/lifegame-cell-strange.gif b/lifegame-cell-strange.gif new file mode 100644 index 0000000..f1fbd77 Binary files /dev/null and b/lifegame-cell-strange.gif differ diff --git a/samples/gameoflife/README.md b/samples/gameoflife/README.md index f840121..71ad63b 100644 --- a/samples/gameoflife/README.md +++ b/samples/gameoflife/README.md @@ -109,7 +109,93 @@ function init() { } ``` -### ステップ2 セルの生存と死滅のルールを適用させよう +### ステップ2 初期状態をランダムにしよう +このままだと、ライフゲームのルールを与えても最初のセルがすべて死んでいるので何も変化がおきません。最初はランダムにセルを生きた状態にしましょう。 +また、簡単化のためキャンバスの端のセルは常に死んだ状態であるとします。 + +どうすればいけそうでしょうか? ちなみにランダムに生死を取り出すには以下の書き方ができます。 + +```js + +const deadOrLive = random(['o','x']); +console.log(deadOrLive) +``` + +
+答え + +```js +// 変数の宣言 +let w; // セルの幅(ピクセル) +let columns; // 列数 +let rows; // 行数 +let cells; // セルの状態2次元配列 + +function setup() { + // フレームレートを24フレーム/秒に設定 + // frameRate(24); + + // キャンバスの大きさを720x400ピクセルに指定 + createCanvas(720, 400); + // 背景色をカラーコード#E0F4FFに指定 + background("#E0F4FF"); + + // セルの幅を20ピクセルに指定 + w = 20; + + // 行数の計算 + rows = 400 / w; + + // 列数の計算 + columns = 720 / w; + + init(); +} + +function draw() { + // 行ループ + for (let i = 0; i < rows; i++) { + // 列ループ + for (let j = 0; j < columns; j++) { + if (cells[i][j] === "x") { + fill(0); // 黒で塗りつぶし + } else { + fill(255); // 白で塗りつぶし + } + rect(j * w, i * w, w, w); + } + } +} + +function init() { + // cellsを20行36列の2次元配列として、"x"で埋めるコード + + // 行数分のundefined要素をもった配列で初期化 + cells = new Array(rows); + + + for (let i = 0; i < rows; i++) { + // さらに要素ごとに、列数分のundefined要素をを持った配列で初期化 + cells[i] = new Array(columns); + + // 列ループ + for (let j = 0; j < columns; j++) { + // 端のセルは常に死んだ状態 + if(i === 0 || j === 0 || i === rows -1 || j === columns - 1){ + cells[i][j] = 'x' + } else { + // 初期状態はランダムに生きている。 + cells[i][j] = random(['o', 'x']); + } + } + } +} +``` +
+ +![ランダムに生きているセル](/samples/gameoflife/random-cells.png) + +### ステップ3 セルの生存と死滅のルールを適用させよう このままだと、単なるピクセルアートな静止画のままです。ライフゲームのアルゴリズムを適用させてこのピクセルアートに命を吹き込みましょう。 @@ -257,15 +343,19 @@ function init(){ // 行数分のundefined要素をもった配列で初期化 cells = new Array(rows); - // 行ループ for (let i = 0; i < rows; i++) { // さらに要素ごとに、列数分のundefined要素をを持った配列で初期化 cells[i] = new Array(columns); // 列ループ for (let j = 0; j < columns; j++) { - // i行目j列目の要素を'x'で埋める - cells[i][j] = 'x' + // 端のセルは常に死んだ状態 + if(i === 0 || j === 0 || i === rows -1 || j === columns - 1){ + cells[i][j] = 'x' + } else { + // 初期状態はランダムに生きている。 + cells[i][j] = random(['o', 'x']); + } } } @@ -458,19 +548,19 @@ function goNextGeneration(){ // そのセルが死んでいたとする。そのセルに隣接するセルのうちちょうど3つが生きていれば、次の世代で、そのセルは誕生する。 if(cells[i][j] === 'x' && neighbors === 3) { - next[i][j] === 'o' + next[i][j] = 'o' } // そのセルが生きていたとする。そのセルに隣接するセルのうち4つ以上のセルが生きていれば、次の世代で、そのセルは過密により死滅する。 else if(cells[i][j] === 'o' && neighbors > 3) { - next[i][j] === 'x' + next[i][j] = 'x' } // そのセルが生きていたとする。そのセルに隣接するセルのうちちょうど2つか3つが生きていれば、次の世代で、そのセルは生存する。 else if(cells[i][j] === 'o' && neighbors >= 2) { - next[i][j] === 'o' + next[i][j] = 'o' } // そのセルが生きていたとする。そのセルに隣接するセルのうち1つ以下しか生きていなければ、次の世代で、そのセルは過疎により死滅する。 - if(cells[i][j] === 'o' && neighbors <= 1) { - next[i][j] === 'x' + else if(cells[i][j] === 'o' && neighbors <= 1) { + next[i][j] = 'x' } } } @@ -478,4 +568,273 @@ function goNextGeneration(){ // 世代を置き換える cells = next; } -``` \ No newline at end of file +``` + +ターゲットのセルの状態と、周辺の生きているセルの数でどうなるのか、あらためて表で整理してみました。 + +||0|1|2|3|4|5| +|---|---|---|---|---|---|---| +|oの場合|過疎x|過疎x|生存o|生存o|過密x|過密x| +|xの場合|x|x|x|誕生o|x|x| + +先程のセルの生死判定は、もう少しスッキリできるかもしれませんね。もし余裕があれば考えてみてください。 + +さて、画面を更新すると、ライフゲームが動きました! + +![ライフゲームが動くが挙動がおかしい](/samples/gameoflife/lifegame-cell-strange.gif) + +しかし、妙ですね。このライフゲーム、終わらないですし、ネットで見るような挙動とは違います。 + +実は、ここには難易度の高いバグが隠れています。それは、オブジェクトや配列の値を代入するときに、値渡しではなく参照渡しになるというバグです。 + +[https://paiza.io]などのオンラインエディタでJSを開いて、以下のコードを実行してみてください。 + +```js +const a = ['a', 'b', 'c'] +const b = a; + +console.log(a) +console.log(b) +``` + +`console.log(a)`と`console.log(b)`には何が出力されますか? + +おそらく両方とも['a','b','c']が表示されるはずです。 + +ここで問題です。bの値を変更したとき、aの値はどうなるでしょうか? + +```js +const a = ['a', 'b', 'c'] +const b = a; + +console.log(a) +console.log(b) + +b[0] = 'b' + +console.log(a) +console.log(b) +``` + +少し考えてみて実行してください。 + +実はこの挙動は参照渡しと呼ばれ、bに代入されるときは配列をコピーして渡しているわけではなく(それは値渡しと呼ばれる)、配列のあるメモリ(の宛先)をbに渡しているのです。だから、bの値を変更すると、直接aの値も変更されてしまいます。 + +
+オブジェクトの場合 +以下のコードを[https://paiza.io]などのオンラインエディタで実行してみてください。 + +```js +const a = { + a: 'a', + b: 'b', + c: 'c' +} +const b = a; + +console.log(a) +console.log(b) + +b.a = 'd'; + +console.log(a) +console.log(b) +``` +
+ +この挙動はメモリ効率は良いのですが、(発見)難易度の高いバグの原因にもなります。プロのプログラマーは配列やオブジェクトを扱うときは最新の注意を払います。 + +ではどうすればよいのでしょうか?オブジェクトや配列自体がコピーされて渡されないのであれば、プログラマーが無理やりコピーして渡してあげればよいのです。 + +コピーの仕方にはいくつかわります(シャローコピーとディープコピー)。しかし今は区別をつけなくて大丈夫です。 + +`goNextGeneration`の最後を次のように書き換えてください。`structuredClone`は(ディープ)コピーをする関数です。 + +```js + // 世代を置き換える + // このとき、cells = nextだけだと、配列は参照渡しなので、 + // nextが書き換わるとcellsの値まで書き換わってしまう。 + // そこで値をコピーして渡す。structuredCloneはコピー関数。 + cells = structuredClone(next); + + // コピーするかわりに参照を入れ替えるやり方もある。こちらのほうが効率はよい。 + // let temp = cells; + // next = temp; + // next = cells; +``` + +
+ここまでのコード + +```js +// 変数の宣言 +let w; +let rows; +let columns; +let cells; +let next; // 次世代のセルの状態を保持する。 + +function setup() { + // フレームレートを24フレーム/秒に設定 + frameRate(24); + + // キャンバスの大きさを720x400ピクセルに指定 + createCanvas(720, 400); + // 背景色をカラーコード#E0F4FFに指定 + background("#E0F4FF"); + + // セルの幅を20ピクセルに指定 + w = 20; + + // 行数の計算 + rows = 400 / w; + + // 列数の計算 + columns = 720 / w; + + // コンソールにcolumnsとrowsの値を出力 + console.log("行数:", rows); + console.log("列数:", columns); + + init(); + console.log(cells); +} + +function draw() { + goNextGeneration(); + + // 行ループ + for (let i = 0; i < rows; i++) { + // 列ループ + for (let j = 0; j < columns; j++) { + if (cells[i][j] === "x") { + fill(0); // 黒で塗りつぶし + } else { + fill(255); // 白で塗りつぶし + } + rect(j * w, i * w, w, w); + } + } +} + +function init() { + // 行数分のundefined要素をもった配列で初期化 + cells = new Array(rows); + + for (let i = 0; i < rows; i++) { + // さらに要素ごとに、列数分のundefined要素をを持った配列で初期化 + cells[i] = new Array(columns); + + // 列ループ + for (let j = 0; j < columns; j++) { + // 端のセルは常に死んだ状態 + if (i === 0 || j === 0 || i === rows - 1 || j === columns - 1) { + cells[i][j] = "x"; + } else { + // 初期状態はランダムに生きている。 + cells[i][j] = random(["o", "x"]); + } + } + } + + // 行数分のundefined要素をもった配列で初期化 + next = new Array(rows); + + // 行ループ + for (let i = 0; i < rows; i++) { + // さらに要素ごとに、列数分のundefined要素をを持った配列で初期化 + next[i] = new Array(columns); + + // 列ループ + for (let j = 0; j < columns; j++) { + // i行目j列目の要素を'x'で埋める + next[i][j] = "x"; + } + } +} + +function goNextGeneration() { + for (let i = 1; i < rows - 1; i++) { + for (let j = 1; j < columns - 1; j++) { + // 周囲のセルの条件をチェック + // 隣接するセルの'o'の数をカウント + let neighbors = 0; + // 左上 + if (cells[i - 1][j - 1] === "o") { + neighbors++; + } + // 真上 + if (cells[i - 1][j] === "o") { + neighbors++; + } + // 右上 + if (cells[i - 1][j + 1] === "o") { + neighbors++; + } + // 左 + if (cells[i][j - 1] === "o") { + neighbors++; + } + // 右 + if (cells[i][j + 1] === "o") { + neighbors++; + } + // 左下 + if (cells[i + 1][j - 1] === "o") { + neighbors++; + } + // 真下 + if (cells[i + 1][j] === "o") { + neighbors++; + } + // 右下 + if (cells[i + 1][j + 1] === "o") { + neighbors++; + } + + // そのセルが死んでいたとする。そのセルに隣接するセルのうちちょうど3つが生きていれば、次の世代で、そのセルは誕生する。 + if (cells[i][j] === "x" && neighbors === 3) { + next[i][j] = "o"; + } + // そのセルが生きていたとする。そのセルに隣接するセルのうち4つ以上のセルが生きていれば、次の世代で、そのセルは過密により死滅する。 + else if (cells[i][j] === "o" && neighbors >= 4) { + next[i][j] = "x"; + } + // そのセルが生きていたとする。そのセルに隣接するセルのうちちょうど2つか3つが生きていれば、次の世代で、そのセルは生存する。 + else if (cells[i][j] === "o" && neighbors >= 2) { + next[i][j] = "o"; + } + // そのセルが生きていたとする。そのセルに隣接するセルのうち1つ以下しか生きていなければ、次の世代で、そのセルは過疎により死滅する。 + else if (cells[i][j] === "o" && neighbors <= 1) { + next[i][j] = "x"; + } + // その他の場合はそのまま + else { + next[i][j] = cells[i][j]; + } + } + } + + // 世代を置き換える + // このとき、cells = nextだけだと、配列は参照渡しなので、 + // nextが書き換わるとcellsの値まで書き換わってしまう。 + // そこで値をコピーして渡す。structuredCloneはコピー関数。 + cells = structuredClone(next); + + // コピーするかわりに参照を入れ替えるやり方もある。こちらの方が効率はよい。 + // let temp = cells; + // next = temp; + // next = cells; +} + +``` +
+ +このサンプルで教えることは以上です。あとは自分で好きにカスタマイズしてみましょう! + +- セルの色や背景色を変えたり +- 透明度を変えたり +- セルの大きさや、数を変えたり +- セルの生存アルゴリズムのルールを変えてみたり + +いろいろできそうですね! \ No newline at end of file diff --git a/samples/gameoflife/complete/my-p5.js b/samples/gameoflife/complete/my-p5.js index b8d942c..25eb318 100644 --- a/samples/gameoflife/complete/my-p5.js +++ b/samples/gameoflife/complete/my-p5.js @@ -46,9 +46,9 @@ function init() { for (let j = 0; j < columns; j++) { // i行目j列目の要素をランダムに埋める // ただし端は死んだ状態にする - if(i === 0 || j === 0 || i === rows - 1 || j === columns - 1){ - cells[i][j] = 'x' - }else { + if (i === 0 || j === 0 || i === rows - 1 || j === columns - 1) { + cells[i][j] = "x"; + } else { cells[i][j] = randomDeadOrLive(); } } @@ -68,10 +68,8 @@ function init() { next[i][j] = "x"; } } - } - function draw() { goNextGeneration(); @@ -91,7 +89,6 @@ function draw() { // 現在のcellsの値をもとに次の世代のcellsの値を決める function goNextGeneration() { - console.log('ij', cells[1][5]) for (let i = 1; i < rows - 1; i++) { for (let j = 1; j < columns - 1; j++) { // 周囲のセルの条件をチェック @@ -130,7 +127,6 @@ function goNextGeneration() { neighbors++; } - // そのセルが死んでいたとする。そのセルに隣接するセルのうちちょうど3つが生きていれば、次の世代で、そのセルは誕生する。 if (cells[i][j] === "x" && neighbors === 3) { next[i][j] = "o"; @@ -144,9 +140,8 @@ function goNextGeneration() { } else if (cells[i][j] === "o" && neighbors <= 1) { next[i][j] = "x"; } else { - next[i][j] = cells[i][j] - } - + next[i][j] = cells[i][j]; + } } } @@ -156,7 +151,7 @@ function goNextGeneration() { // そこで値をコピーして渡す。structuredCloneはコピー関数。 cells = structuredClone(next); - // コピーするかわりに参照を入れ替えるやり方もある + // コピーするかわりに参照を入れ替えるやり方もある。こちらの方が効率はよい。 // let temp = cells; // next = temp; // next = cells; diff --git a/samples/gameoflife/lifegame-cell-strange.gif b/samples/gameoflife/lifegame-cell-strange.gif new file mode 100644 index 0000000..1d123f7 Binary files /dev/null and b/samples/gameoflife/lifegame-cell-strange.gif differ diff --git a/samples/gameoflife/random-cells.png b/samples/gameoflife/random-cells.png new file mode 100644 index 0000000..087bf6c Binary files /dev/null and b/samples/gameoflife/random-cells.png differ diff --git a/samples/gameoflife/ready/my-p5.js b/samples/gameoflife/ready/my-p5.js index f72a5d7..d212f7c 100644 --- a/samples/gameoflife/ready/my-p5.js +++ b/samples/gameoflife/ready/my-p5.js @@ -1,16 +1,159 @@ // 変数の宣言 +let w; +let rows; +let columns; +let cells; +let next; // 次世代のセルの状態を保持する。 function setup() { // フレームレートを24フレーム/秒に設定 - // frameRate(24); + frameRate(24); // キャンバスの大きさを720x400ピクセルに指定 createCanvas(720, 400); // 背景色をカラーコード#E0F4FFに指定 background("#E0F4FF"); + // セルの幅を20ピクセルに指定 + w = 20; + + // 行数の計算 + rows = 400 / w; + + // 列数の計算 + columns = 720 / w; + + // コンソールにcolumnsとrowsの値を出力 + console.log("行数:", rows); + console.log("列数:", columns); + + init(); + console.log(cells); } function draw() { + goNextGeneration(); + + // 行ループ + for (let i = 0; i < rows; i++) { + // 列ループ + for (let j = 0; j < columns; j++) { + if (cells[i][j] === "x") { + fill(0); // 黒で塗りつぶし + } else { + fill(255); // 白で塗りつぶし + } + rect(j * w, i * w, w, w); + } + } +} + +function init() { + // 行数分のundefined要素をもった配列で初期化 + cells = new Array(rows); + + for (let i = 0; i < rows; i++) { + // さらに要素ごとに、列数分のundefined要素をを持った配列で初期化 + cells[i] = new Array(columns); + + // 列ループ + for (let j = 0; j < columns; j++) { + // 端のセルは常に死んだ状態 + if (i === 0 || j === 0 || i === rows - 1 || j === columns - 1) { + cells[i][j] = "x"; + } else { + // 初期状態はランダムに生きている。 + cells[i][j] = random(["o", "x"]); + } + } + } + + // 行数分のundefined要素をもった配列で初期化 + next = new Array(rows); + + // 行ループ + for (let i = 0; i < rows; i++) { + // さらに要素ごとに、列数分のundefined要素をを持った配列で初期化 + next[i] = new Array(columns); + + // 列ループ + for (let j = 0; j < columns; j++) { + // i行目j列目の要素を'x'で埋める + next[i][j] = "x"; + } + } +} + +function goNextGeneration() { + for (let i = 1; i < rows - 1; i++) { + for (let j = 1; j < columns - 1; j++) { + // 周囲のセルの条件をチェック + // 隣接するセルの'o'の数をカウント + let neighbors = 0; + // 左上 + if (cells[i - 1][j - 1] === "o") { + neighbors++; + } + // 真上 + if (cells[i - 1][j] === "o") { + neighbors++; + } + // 右上 + if (cells[i - 1][j + 1] === "o") { + neighbors++; + } + // 左 + if (cells[i][j - 1] === "o") { + neighbors++; + } + // 右 + if (cells[i][j + 1] === "o") { + neighbors++; + } + // 左下 + if (cells[i + 1][j - 1] === "o") { + neighbors++; + } + // 真下 + if (cells[i + 1][j] === "o") { + neighbors++; + } + // 右下 + if (cells[i + 1][j + 1] === "o") { + neighbors++; + } + + // そのセルが死んでいたとする。そのセルに隣接するセルのうちちょうど3つが生きていれば、次の世代で、そのセルは誕生する。 + if (cells[i][j] === "x" && neighbors === 3) { + next[i][j] = "o"; + } + // そのセルが生きていたとする。そのセルに隣接するセルのうち4つ以上のセルが生きていれば、次の世代で、そのセルは過密により死滅する。 + else if (cells[i][j] === "o" && neighbors >= 4) { + next[i][j] = "x"; + } + // そのセルが生きていたとする。そのセルに隣接するセルのうちちょうど2つか3つが生きていれば、次の世代で、そのセルは生存する。 + else if (cells[i][j] === "o" && neighbors >= 2) { + next[i][j] = "o"; + } + // そのセルが生きていたとする。そのセルに隣接するセルのうち1つ以下しか生きていなければ、次の世代で、そのセルは過疎により死滅する。 + else if (cells[i][j] === "o" && neighbors <= 1) { + next[i][j] = "x"; + } + // その他の場合はそのまま + else { + next[i][j] = cells[i][j]; + } + } + } + + // 世代を置き換える + // このとき、cells = nextだけだと、配列は参照渡しなので、 + // nextが書き換わるとcellsの値まで書き換わってしまう。 + // そこで値をコピーして渡す。structuredCloneはコピー関数。 + cells = structuredClone(next); + // コピーするかわりに参照を入れ替えるやり方もある。こちらの方が効率はよい。 + // let temp = cells; + // next = temp; + // next = cells; } diff --git a/samples/pixel-art/README.md b/samples/pixel-art/README.md index 8d4ec27..4334e90 100644 --- a/samples/pixel-art/README.md +++ b/samples/pixel-art/README.md @@ -25,7 +25,7 @@ LivePreview で ready フォルダの`index.html`を開く。青い背景のキ ### ステップ 2 セルで埋め尽くしましょう -この何もない背景を正方形の細胞(セル)で埋め尽くします。ピクセルアートはセルの数が多ければ多いほど面白くなります。今回はセルの1つの幅を 20 ピクセルとします。 +この何もない背景を正方形のセルで埋め尽くします。ピクセルアートはセルの数が多ければ多いほど面白くなります。今回はセルの1つの幅を 20 ピクセルとします。 キャンバスに敷き詰めるセルの数は