第7章プロジェクト:人工生命
[...] 機械が思考できるかどうかという問題は [...] 潜水艦が泳げるかどうかという問題とほぼ同じくらい的外れである。
「プロジェクト」の章では、新しい理論であなたを叩きのめすのを少しの間やめ、代わりに一緒にプログラムを作成していきます。 プログラミングを学ぶ上で理論は不可欠ですが、それは、重要でないプログラムを読んで理解することと並行して行われるべきです。
この章のプロジェクトは、仮想的な生態系、つまり、動き回り、生存のために闘う生き物で満たされた小さな世界を構築することです。
定義
このタスクを管理しやすくするために、*世界*の概念を根本的に単純化します。 つまり、世界は二次元のグリッドであり、各エンティティはグリッドの1つの正方形全体を占めます。 すべての*ターン*で、生き物はすべて行動を起こす機会を得ます。
したがって、時間と空間の両方を固定サイズの単位に分割します。空間には正方形、時間にはターンです。 もちろん、これはやや粗雑で不正確な近似です。 しかし、私たちのシミュレーションは正確であることではなく、面白くあることを意図しているので、このような近道は自由に取ることができます。
*プラン*、つまり、正方形ごとに1文字を使用して世界のグリッドをレイアウトする文字列の配列を使用して、世界を定義できます。
var plan = ["############################", "# # # o ##", "# #", "# ##### #", "## # # ## #", "### ## # #", "# ### # #", "# #### #", "# ## o #", "# o # o ### #", "# # #", "############################"];
このプランの「#」文字は壁と岩を表し、「o」文字は生き物を表します。 スペースは、ご想像のとおり、空の空間です。
プラン配列を使用して、ワールドオブジェクトを作成できます。 このようなオブジェクトは、世界のサイズと内容を追跡します。 これには、世界を(それが基づいていたプランと同様の)印刷可能な文字列に変換するtoString
メソッドがあり、これにより、内部で何が起こっているかを確認できます。 ワールドオブジェクトには、その中のすべての生き物が1ターンを実行し、その行動を反映するようにワールドを更新するturn
メソッドもあります。
空間の表現
世界をモデル化するグリッドは、固定の幅と高さを持っています。 正方形は、x座標とy座標によって識別されます。 これらの座標ペアを表すために、単純な型であるVector
(前の章の演習で見たとおり)を使用します。
function Vector(x, y) { this.x = x; this.y = y; } Vector.prototype.plus = function(other) { return new Vector(this.x + other.x, this.y + other.y); };
次に、グリッド自体をモデル化するオブジェクト型が必要です。 グリッドは世界の一部ですが、ワールドオブジェクト自体をシンプルにするために、それを別のオブジェクト(ワールドオブジェクトのプロパティになります)にしています。 世界は世界に関連することを懸念する必要があり、グリッドはグリッドに関連することを懸念する必要があります。
値のグリッドを格納するために、いくつかのオプションがあります。 行配列の配列を使用し、2つのプロパティアクセスを使用して特定の正方形にアクセスできます。次のようにします。
var grid = [["top left", "top middle", "top right"], ["bottom left", "bottom middle", "bottom right"]]; console.log(grid[1][2]); // → bottom right
または、サイズが幅×高さの単一の配列を使用し、( *x* , *y* ) の要素が配列内の *x* + ( *y* × 幅) の位置にあると決定することもできます。
var grid = ["top left", "top middle", "top right", "bottom left", "bottom middle", "bottom right"]; console.log(grid[2 + (1 * 3)]); // → bottom right
この配列への実際のアクセスはグリッドオブジェクト型のメソッドにラップされるため、どちらのアプローチを採用しても外部コードには影響しません。 配列の作成がはるかに簡単になるため、2番目の表現を選択しました。 Array
コンストラクターを単一の数字を аргументとして呼び出すと、指定された長さの新しい空の配列が作成されます。
このコードは、いくつかの基本的なメソッドを持つGrid
オブジェクトを定義しています。
function Grid(width, height) { this.space = new Array(width * height); this.width = width; this.height = height; } Grid.prototype.isInside = function(vector) { return vector.x >= 0 && vector.x < this.width && vector.y >= 0 && vector.y < this.height; }; Grid.prototype.get = function(vector) { return this.space[vector.x + this.width * vector.y]; }; Grid.prototype.set = function(vector, value) { this.space[vector.x + this.width * vector.y] = value; };
var grid = new Grid(5, 5); console.log(grid.get(new Vector(1, 1))); // → undefined grid.set(new Vector(1, 1), "X"); console.log(grid.get(new Vector(1, 1))); // → X
生き物のプログラミングインターフェース
World
コンストラクターを開始する前に、その中に住む生き物オブジェクトについて、より具体的に説明する必要があります。 世界は生き物にどのような行動を取りたいかを尋ねると述べました。 これは次のように機能します。各生き物オブジェクトには、呼び出されると*アクション*を返すact
メソッドがあります。 アクションとは、生き物が実行したいアクションのタイプ(たとえば、"move"
)を指定するtype
プロパティを持つオブジェクトです。 アクションには、生き物が移動したい方向などの追加情報が含まれている場合もあります。
生き物は非常に近視眼的であり、グリッド上で自分の周りの正方形しか見ることができません。 しかし、この限られた視界でさえ、どの行動をとるかを決定する際に役立つ可能性があります。 act
メソッドが呼び出されると、生き物が周囲を検査できる*ビュー*オブジェクトが渡されます。 周囲の8つの正方形を方位で名前を付けます。北は"n"
、北東は"ne"
などです。 方向名から座標オフセットにマップするために使用するオブジェクトを次に示します。
var directions = { "n": new Vector( 0, -1), "ne": new Vector( 1, -1), "e": new Vector( 1, 0), "se": new Vector( 1, 1), "s": new Vector( 0, 1), "sw": new Vector(-1, 1), "w": new Vector(-1, 0), "nw": new Vector(-1, -1) };
ビューオブジェクトには、方向を取り、文字を返すメソッドlook
があります。たとえば、その方向に壁がある場合は"#"
、何もない場合は" "
(スペース)を返します。 オブジェクトはまた、便利なメソッドfind
とfindAll
を提供します。 両方はマップ文字を引数として取ります。 最初のメソッドは、生き物の隣に見つかった文字の方向を返し、そのような方向が存在しない場合はnull
を返します。 2番目のメソッドは、その文字を持つすべての方向を含む配列を返します。 たとえば、壁の左側(西)に座っている生き物は、findAll
を"#"
文字を引数としてビューオブジェクトで呼び出すと、["ne", "e", "se"]
を取得します。
障害物にぶつかるまでひたすら鼻歌を歌い続け、ランダムな方向に跳ね返る、単純で愚かな生き物を次に示します。
function randomElement(array) { return array[Math.floor(Math.random() * array.length)]; } var directionNames = "n ne e se s sw w nw".split(" "); function BouncingCritter() { this.direction = randomElement(directionNames); }; BouncingCritter.prototype.act = function(view) { if (view.look(this.direction) != " ") this.direction = view.find(" ") || "s"; return {type: "move", direction: this.direction}; };
randomElement
ヘルパー関数は、Math.random
といくつかの算術演算を使用してランダムなインデックスを取得し、配列からランダムな要素を選択するだけです。 ランダム性はシミュレーションで役立つ可能性があるため、後で再びこれを使用します。
ランダムな方向を選択するために、BouncingCritter
コンストラクターは方向名の配列でrandomElement
を呼び出します。 前述のように定義したdirections
オブジェクトからObject.keys
を使用してこの配列を取得することもできますが、プロパティがリストされる順序については保証がありません。 ほとんどの状況では、最新のJavaScriptエンジンは定義された順序でプロパティを返しますが、必須ではありません。
act
メソッドの「|| "s"
」は、生き物が周囲に空のスペースがない状態で(たとえば、他の生き物によって隅に追いやられた場合)閉じ込められた場合に、this.direction
が値null
を取得しないようにするためにあります。
ワールドオブジェクト
これで、World
オブジェクトタイプの作業を開始できます。 コンストラクターは、プラン(前述の世界のグリッドを表す文字列の配列)と*凡例*を引数として取ります。 凡例とは、マップの各文字の意味を教えてくれるオブジェクトです。 これには、スペース文字を除くすべての文字のコンストラクターが含まれています。スペース文字は常にnull
を参照し、これは空のスペースを表すために使用する値です。
function elementFromChar(legend, ch) { if (ch == " ") return null; var element = new legend[ch](); element.originChar = ch; return element; } function World(map, legend) { var grid = new Grid(map[0].length, map.length); this.grid = grid; this.legend = legend; map.forEach(function(line, y) { for (var x = 0; x < line.length; x++) grid.set(new Vector(x, y), elementFromChar(legend, line[x])); }); }
elementFromChar
では、最初に文字のコンストラクターを調べてnew
を適用することにより、適切なタイプのインスタンスを作成します。 次に、originChar
プロパティを追加して、要素が元々どの文字から作成されたかを簡単にわかるようにします。
世界のtoString
メソッドを実装するときに、このoriginChar
プロパティが必要です。 このメソッドは、グリッド上の正方形に対して2次元ループを実行することにより、世界の現在の状態からマップのような文字列を構築します。
function charFromElement(element) { if (element == null) return " "; else return element.originChar; } World.prototype.toString = function() { var output = ""; for (var y = 0; y < this.grid.height; y++) { for (var x = 0; x < this.grid.width; x++) { var element = this.grid.get(new Vector(x, y)); output += charFromElement(element); } output += "\n"; } return output; };
壁は単純なオブジェクトです。スペースを占めるためだけに使用され、act
メソッドはありません。
function Wall() {}
章の前のプランに基づいてインスタンスを作成し、toString
を呼び出すことにより、World
オブジェクトを試してみると、入力したプランと非常によく似た文字列が得られます。
var world = new World(plan, {"#": Wall, "o": BouncingCritter}); console.log(world.toString()); // → ############################ // # # # o ## // # # // # ##### # // ## # # ## # // ### ## # # // # ### # # // # #### # // # ## o # // # o # o ### # // # # # // ############################
thisとそのスコープ
World
コンストラクターには、forEach
の呼び出しが含まれています。 注意すべき興味深い点の1つは、forEach
に渡された関数の内部では、コンストラクターの関数スコープに直接いないことです。 各関数呼び出しは独自のthis
バインディングを取得するため、内部関数のthis
は、外部のthis
が参照する新しく構築されたオブジェクトを*参照しません*。 実際、関数がメソッドとして呼び出されない場合、this
はグローバルオブジェクトを参照します。
これは、ループ内からグリッドにアクセスするためにthis.grid
を記述できないことを意味します。 代わりに、外部関数は通常のローカル変数grid
を作成し、内部関数はそれを介してグリッドにアクセスします。
これは、JavaScriptの設計上のちょっとした失敗です。 幸いなことに、言語の次のバージョンでは、この問題の解決策が提供されています。 一方、回避策があります。一般的なパターンは、var self = this
と言うことであり、それ以降は、通常の変数であり、したがって内部関数から見えるself
を参照します。
別の解決策は、バインドする明示的なthis
オブジェクトを提供できるbind
メソッドを使用することです。
var test = { prop: 10, addPropTo: function(array) { return array.map(function(elt) { return this.prop + elt; }.bind(this)); } }; console.log(test.addPropTo([5])); // → [15]
map
に渡される関数は bind
呼び出しの結果であるため、その this
は bind
に渡された最初の引数、つまり外側の関数の this
値(test
オブジェクトを保持している)にバインドされています。
forEach
や map
など、配列のほとんどの標準的な高階メソッドは、反復関数への呼び出しに this
を提供するためにも使用できる、オプションの 2 番目の引数を取ります。そのため、前の例をもう少し簡単な方法で表現できます。
var test = { prop: 10, addPropTo: function(array) { return array.map(function(elt) { return this.prop + elt; }, this); // ← no bind } }; console.log(test.addPropTo([5])); // → [15]
これは、このような*コンテキスト*パラメータをサポートする高階関数に対してのみ機能します。サポートしていない場合は、他のアプローチのいずれかを使用する必要があります。
私たち自身で作成する高階関数では、引数として渡された関数を呼び出すために call
メソッドを使用することで、このようなコンテキストパラメータをサポートできます。たとえば、Grid
型の forEach
メソッドを次に示します。これは、グリッド内の null または undefined でない各要素に対して、指定された関数を呼び出します。
Grid.prototype.forEach = function(f, context) { for (var y = 0; y < this.height; y++) { for (var x = 0; x < this.width; x++) { var value = this.space[x + y * this.width]; if (value != null) f.call(context, value, new Vector(x, y)); } } };
生命のアニメーション化
次のステップは、クリッターに動作の機会を与える、ワールドオブジェクトの turn
メソッドを作成することです。これは、定義したばかりの forEach
メソッドを使用してグリッドを調べ、act
メソッドを持つオブジェクトを探します。見つかった場合、turn
はそのメソッドを呼び出してアクションオブジェクトを取得し、有効な場合はアクションを実行します。今のところ、"move"
アクションのみが理解されます。
このアプローチには潜在的な問題が 1 つあります。わかりますか?クリッターを見つけたらすぐに移動できるようにすると、まだ見ていないマスに移動する可能性があり、そのマスに到達したときに*再び*移動できるようにしてしまうことになります。そのため、すでにターンが終了したクリッターの配列を保持し、再びそれらを見つけた場合は無視する必要があります。
World.prototype.turn = function() { var acted = []; this.grid.forEach(function(critter, vector) { if (critter.act && acted.indexOf(critter) == -1) { acted.push(critter); this.letAct(critter, vector); } }, this); };
内部関数内で正しい this
にアクセスできるように、グリッドの forEach
メソッドの 2 番目のパラメータを使用します。 letAct
メソッドには、クリッターの移動を可能にする実際のロジックが含まれています。
World.prototype.letAct = function(critter, vector) { var action = critter.act(new View(this, vector)); if (action && action.type == "move") { var dest = this.checkDestination(action, vector); if (dest && this.grid.get(dest) == null) { this.grid.set(vector, null); this.grid.set(dest, critter); } } }; World.prototype.checkDestination = function(action, vector) { if (directions.hasOwnProperty(action.direction)) { var dest = vector.plus(directions[action.direction]); if (this.grid.isInside(dest)) return dest; } };
まず、クリッターに動作するように要求し、ワールドとクリッターのワールドにおける現在の位置を知っているビューオブジェクトを渡します(View
は後で定義します)。 act
メソッドは、何らかの種類のアクションを返します。
アクションの type
が "move"
でない場合は無視されます。 "move"
であり、有効な方向を参照する direction
プロパティがあり、*かつ*その方向のマスが空(null)の場合、クリッターが以前いたマスを null に設定し、クリッターを移動先のマスに格納します。
letAct
は、無意味な入力を無視するように注意していることに注意してください。アクションの direction
プロパティが有効であるとか、type
プロパティが意味をなすとは想定していません。この種の*防御的*プログラミングは、状況によっては意味があります。主な理由は、ユーザー入力やファイル入力など、制御できないソースからの入力を検証するためですが、サブシステムを相互に分離するためにも役立ちます。この場合、クリッター自体がずさんにプログラムされていても問題ありません。意図したアクションが意味をなすかどうかを確認する必要はありません。アクションを要求するだけで、ワールドがそれを許可するかどうかを判断します。
これらの 2 つのメソッドは、World
オブジェクトの外部インターフェースの一部ではありません。内部的な詳細です。一部の言語では、特定のメソッドとプロパティを明示的に*プライベート*として宣言し、オブジェクトの外部から使用しようとするとエラーを通知する方法を提供しています。JavaScript にはありませんので、オブジェクトのインターフェースの一部であるものを記述するには、他の形式のコミュニケーションに頼る必要があります。外部プロパティと内部プロパティを区別するために、たとえば、すべての内部プロパティにアンダースコア文字(_)を付けるなど、命名規則を使用すると役立つ場合があります。これにより、オブジェクトのインターフェースの一部ではないプロパティを誤って使用した場合に、簡単に見つけることができます。
function View(world, vector) { this.world = world; this.vector = vector; } View.prototype.look = function(dir) { var target = this.vector.plus(directions[dir]); if (this.world.grid.isInside(target)) return charFromElement(this.world.grid.get(target)); else return "#"; }; View.prototype.findAll = function(ch) { var found = []; for (var dir in directions) if (this.look(dir) == ch) found.push(dir); return found; }; View.prototype.find = function(ch) { var found = this.findAll(ch); if (found.length == 0) return null; return randomElement(found); };
look
メソッドは、見ようとしている座標を特定し、グリッド内にある場合は、そこに存在する要素に対応する文字を見つけます。グリッド外の座標の場合、look
は壁があるかのように見せかけるため、壁で囲まれていないワールドを定義した場合でも、クリッターは端から歩こうとはしません。
動く
先ほどワールドオブジェクトをインスタンス化しました。必要なメソッドをすべて追加したので、実際にワールドを動かすことができるはずです。
for (var i = 0; i < 5; i++) { world.turn(); console.log(world.toString()); } // → … five turns of moving critters
ただし、マップのコピーをたくさん印刷するだけでは、ワールドを観察するにはあまり適していません。そのため、サンドボックスには animateWorld
関数が用意されており、停止ボタンを押すまで、1 秒間に 3 ターン移動する画面上のアニメーションとしてワールドを実行します。
animateWorld(world); // → … life!
animateWorld
の実装は今のところ謎のままですが、Web ブラウザでの JavaScript 統合について説明している本書の後の章を読めば、それほど不思議なものではなくなるでしょう。
より多くの生命体
しばらく見ていると、私たちのワールドの劇的なハイライトは、2 匹のクリッターがお互いに跳ね返るときです。他に興味深い行動形態は考えられますか?
私が思いついたのは、壁に沿って移動するクリッターです。概念的には、クリッターは左手(足、触手など)を壁につけたまま、壁に沿って移動します。これは、実装するのがそれほど簡単ではないことがわかりました。
方位を使って「計算」できる必要があります。方向は文字列のセットによってモデル化されているため、相対方向を計算するための独自の演算(dirPlus
)を定義する必要があります。そのため、dirPlus("n", 1)
は北から時計回りに 45 度回転することを意味し、"ne"
になります。同様に、dirPlus("s", -2)
は南から反時計回りに 90 度回転することを意味し、東になります。
function dirPlus(dir, n) { var index = directionNames.indexOf(dir); return directionNames[(index + n + 8) % 8]; } function WallFollower() { this.dir = "s"; } WallFollower.prototype.act = function(view) { var start = this.dir; if (view.look(dirPlus(this.dir, -3)) != " ") start = this.dir = dirPlus(this.dir, -2); while (view.look(this.dir) != " ") { this.dir = dirPlus(this.dir, 1); if (this.dir == start) break; } return {type: "move", direction: this.dir}; };
act
メソッドは、クリッターの左側を開始点として時計回りに空のマスが見つかるまで、クリッターの周囲を「スキャン」するだけです。そして、その空のマスの向きに移動します。
物事を複雑にしているのは、クリッターが開始位置として、または別のクリッターの周りを歩いた結果として、空の空間の真ん中にたどり着く可能性があることです。空の空間で先ほど説明したアプローチを適用すると、かわいそうなクリッターはすべてのステップで左に回転し続け、円を描いて走ってしまいます。
そのため、クリッターの後ろと左側のスペースが空でない場合、つまりクリッターがある種の障害物を gerade 通過したように見える場合にのみ、左へのスキャンを開始するための追加チェック(if
ステートメント)があります。そうでない場合、クリッターは正面から直接スキャンを開始するため、空の空間ではまっすぐ歩きます。
最後に、ループを 1 回通過するごとに this.dir
と start
を比較するテストがあり、クリッターが壁に囲まれている場合、または他のクリッターに囲まれて空のマスが見つからない場合に、ループが永遠に実行されないようにします。
animateWorld(new World( ["############", "# # #", "# ~ ~ #", "# ## #", "# ## o####", "# #", "############"], {"#": Wall, "~": WallFollower, "o": BouncingCritter} ));
よりリアルなシミュレーション
私たちのワールドでの生活をより面白くするために、食物と生殖の概念を追加します。ワールド内のすべての生き物は、新しいプロパティである energy
を取得します。これは、アクションを実行することで減少し、何かを食べることで増加します。クリッターが十分なエネルギーを持っていると、繁殖して同じ種類の新しいクリッターを生成できます。物事をシンプルにするために、私たちのワールドのクリッターは無性生殖で、すべて単独で繁殖します。
クリッターが動き回って互いに食べ合うだけの場合、ワールドはすぐにエントロピー増大の法則に従い、エネルギーを使い果たし、生命のない荒れ地になってしまいます。これが(少なくともあまりにも早く)起こらないようにするために、ワールドに植物を追加します。植物は動きません。光合成を使って成長(つまり、エネルギーを増やす)し、繁殖するだけです。
これを機能させるには、異なる letAct
メソッドを持つワールドが必要です。World
プロトタイプのメソッドを置き換えることもできますが、壁に沿って移動するクリッターを使ったシミュレーションに非常に愛着があり、その古いワールドを壊したくありません。
1 つの解決策は、継承を使用することです。プロトタイプは World
プロトタイプに基づいていますが、letAct
メソッドをオーバーライドする新しいコンストラクタ LifelikeWorld
を作成します。新しい letAct
メソッドは、実際のアクションの実行を、actionTypes
オブジェクトに格納されているさまざまな関数に委任します。
function LifelikeWorld(map, legend) { World.call(this, map, legend); } LifelikeWorld.prototype = Object.create(World.prototype); var actionTypes = Object.create(null); LifelikeWorld.prototype.letAct = function(critter, vector) { var action = critter.act(new View(this, vector)); var handled = action && action.type in actionTypes && actionTypes[action.type].call(this, critter, vector, action); if (!handled) { critter.energy -= 0.2; if (critter.energy <= 0) this.grid.set(vector, null); } };
新しい letAct
メソッドは、まずアクションが überhaupt 返されたかどうか、次にこのタイプのアクションのハンドラ関数が存在するかどうか、最後にそのハンドラが true を返したかどうかを確認し、アクションが正常に処理されたことを示します。call
を使用して、this
バインディングを介してハンドラがワールドにアクセスできるようにしていることに注意してください。
何らかの理由でアクションがうまくいかなかった場合、デフォルトのアクションは、生き物が単に待つことです。エネルギーの 5 分の 1 ポイントを失い、エネルギーレベルがゼロ以下に下がると、生き物は死に、グリッドから削除されます。
アクションハンドラ
生き物が実行できる最も簡単なアクションは、植物が使用する "grow"
です。{type: "grow"}
のようなアクションオブジェクトが返されると、次のハンドラメソッドが呼び出されます。
actionTypes.grow = function(critter) { critter.energy += 0.5; return true; };
成長は常に成功し、植物のエネルギーレベルに 0.5 ポイントを追加します。
actionTypes.move = function(critter, vector, action) { var dest = this.checkDestination(action, vector); if (dest == null || critter.energy <= 1 || this.grid.get(dest) != null) return false; critter.energy -= 1; this.grid.set(vector, null); this.grid.set(dest, critter); return true; };
このアクションは、以前に定義した checkDestination
メソッドを使用して、アクションが有効な宛先を提供するかどうかを最初に確認します。そうでない場合、または宛先が空でない場合、またはクリッターに必要なエネルギーがない場合、move
は false を返し、アクションが実行されなかったことを示します。そうでない場合は、クリッターを移動し、エネルギーコストを差し引きます。
actionTypes.eat = function(critter, vector, action) { var dest = this.checkDestination(action, vector); var atDest = dest != null && this.grid.get(dest); if (!atDest || atDest.energy == null) return false; critter.energy += atDest.energy; this.grid.set(dest, null); return true; };
他のクリッターを食べることも、有効な目的地 squares を提供することを含みます。今回は、目的地は空ではなく、クリッター(壁ではなく、壁は食べられません)のようなエネルギーを持つ何かを含んでいる必要があります。もしそうなら、食べられたものからのエネルギーは食べるものに移され、犠牲者はグリッドから削除されます。
actionTypes.reproduce = function(critter, vector, action) { var baby = elementFromChar(this.legend, critter.originChar); var dest = this.checkDestination(action, vector); if (dest == null || critter.energy <= 2 * baby.energy || this.grid.get(dest) != null) return false; critter.energy -= 2 * baby.energy; this.grid.set(dest, baby); return true; };
繁殖には、生まれたばかりのクリッターのエネルギーレベルの2倍の費用がかかります。そのため、最初にクリッター自身の元の文字で `elementFromChar` を使用して(仮説的な)赤ちゃんを作成します。赤ちゃんができたら、そのエネルギーレベルを見つけ、親がそれをうまく世に出すのに十分なエネルギーを持っているかどうかをテストできます。また、有効な(そして空の)目的地も必要です。
すべてが問題なければ、赤ちゃんはグリッドに配置され(もはや仮説ではなくなります)、エネルギーが消費されます。
新しい世界に住まわせる
これで、これらのよりリアルな生き物をシミュレートするためのフレームワークができました。古い世界のクリッターをそこに入れることもできますが、エネルギー特性がないため、ただ死んでしまいます。ですから、新しいものを作ってみましょう。最初に、かなり単純な生命体である植物を作成します。
function Plant() { this.energy = 3 + Math.random() * 4; } Plant.prototype.act = function(view) { if (this.energy > 15) { var space = view.find(" "); if (space) return {type: "reproduce", direction: space}; } if (this.energy < 20) return {type: "grow"}; };
植物は、すべて同じターンで繁殖しないようにランダム化された、3〜7のエネルギーレベルで始まります。植物が15エネルギーポイントに達し、近くに空きスペースがある場合、その空きスペースに繁殖します。植物が繁殖できない場合は、エネルギーレベル20に達するまで成長し続けます。
function PlantEater() { this.energy = 20; } PlantEater.prototype.act = function(view) { var space = view.find(" "); if (this.energy > 60 && space) return {type: "reproduce", direction: space}; var plant = view.find("*"); if (plant) return {type: "eat", direction: plant}; if (space) return {type: "move", direction: space}; };
植物には `*` 文字を使用するので、このクリーチャーは食べ物を探すときにそれを探します。
生命を吹き込む
これで、新しい世界を試すのに十分な要素が揃いました。次の地図を、草食動物の群れ、いくつかの岩、そして緑豊かな植物が生い茂る草の谷として想像してみてください。
var valley = new LifelikeWorld( ["############################", "##### ######", "## *** **##", "# *##** ** O *##", "# *** O ##** *#", "# O ##*** #", "# ##** #", "# O #* #", "#* #** O #", "#*** ##** O **#", "##**** ###*** *###", "############################"], {"#": Wall, "O": PlantEater, "*": Plant} );
animateWorld(valley);
ほとんどの場合、植物は非常に急速に増殖して拡大しますが、その後、食物の豊富さが草食動物の個体数爆発を引き起こし、草食動物はすべてまたはほぼすべての植物を一掃し、クリッターの大量飢餓をもたらします。時々、生態系は回復し、別のサイクルが始まります。また、一方の種が完全に死滅することもあります。草食動物であれば、空間全体が植物で満たされます。植物であれば、残りのクリッターは飢え、谷は荒涼とした荒れ地になります。ああ、自然の残酷さ。
練習問題
人工的な愚かさ
私たちの世界の住人が数分後に絶滅するのは少し depressing です。これに対処するために、より賢い草食動物を作成してみることができます。
私たちの草食動物には、いくつかの明らかな問題があります。第一に、彼らはひどく貪欲で、地元の植物を一掃するまで、目にするすべての植物を詰め込みます。第二に、彼らのランダムな動き(`view.find` メソッドは、複数の方向が一致するとランダムな方向を返すことを思い出してください)は、彼らを効果的によろめき回らせ、近くに植物がない場合に飢えさせます。そして最後に、彼らは非常に速く繁殖するため、豊かさと飢饉の間のサイクルが非常に激しくなります。
これらのポイントの1つ以上に対処しようとする新しいクリッタータイプを作成し、それを谷の世界の古い `PlantEater` タイプに置き換えます。それがどのように機能するかを確認してください。必要に応じて、さらに微調整してください。
// Your code here function SmartPlantEater() {} animateWorld(new LifelikeWorld( ["############################", "##### ######", "## *** **##", "# *##** ** O *##", "# *** O ##** *#", "# O ##*** #", "# ##** #", "# O #* #", "#* #** O #", "#*** ##** O **#", "##**** ###*** *###", "############################"], {"#": Wall, "O": SmartPlantEater, "*": Plant} ));
貪欲の問題は、いくつかの方法で攻撃できます。クリッターは特定のエネルギーレベルに達すると食べるのをやめることができます。または、Nターンごとにのみ食べることができます(クリーチャーオブジェクトのプロパティに最後の食事からのターン数のカウンターを保持することにより)。あるいは、植物が完全に絶滅しないようにするために、動物は近くに少なくとも1つの他の植物を見ない限り、植物を食べることを拒否することができます(ビューで `findAll` メソッドを使用します)。これらの組み合わせ、またはまったく異なる戦略も機能する可能性があります。
クリッターをより効果的に移動させるには、古い、エネルギーのない世界のクリッターから移動戦略の1つを盗むことができます。バウンス動作と壁追従動作の両方が、完全にランダムなよろめきよりもはるかに広い範囲の動きを示しました。
クリーチャーの繁殖を遅くするのは簡単です。彼らが繁殖する最小エネルギーレベルを上げるだけです。もちろん、生態系をより安定させることは、それをより退屈にすることにもなります。少数の太った、動かないクリッターが永遠に植物の海をむしゃむしゃ食べて繁殖することがなければ、それは非常に安定した生態系になります。しかし、誰もそれを見たくありません。
捕食者
深刻な生態系はすべて、単一のリンクよりも長い食物連鎖を持っています。草食動物のクリッターを食べて生き残る別のクリッターを書いてください。複数レベルのサイクルがあるため、安定性を達成するのがさらに難しいことに気付くでしょう。少なくともしばらくの間、生態系をスムーズに実行するための戦略を見つけてください。
役立つことの1つは、世界を大きくすることです。このようにして、局所的な人口の増加または減少が種を完全に一掃する可能性は低くなり、少数の捕食者集団を維持するために必要な比較的大規模な獲物集団のためのスペースがあります。
// Your code here function Tiger() {} animateWorld(new LifelikeWorld( ["####################################################", "# #### **** ###", "# * @ ## ######## OO ##", "# * ## O O **** *#", "# ##* ########## *#", "# ##*** * **** **#", "#* ** # * *** ######### **#", "#* ** # * # * **#", "# ## # O # *** ######", "#* @ # # * O # #", "#* # ###### ** #", "### **** *** ** #", "# O @ O #", "# * ## ## ## ## ### * #", "# ** # * ##### O #", "## ** O O # # *** *** ### ** #", "### # ***** ****#", "####################################################"], {"#": Wall, "@": Tiger, "O": SmartPlantEater, // from previous exercise "*": Plant} ));
前の演習でうまくいったのと同じトリックの多くが、ここでも適用されます。捕食者を大きく(多くのエネルギー)し、ゆっくりと繁殖させることをお勧めします。それは彼らを草食動物が不足しているときの飢餓の期間に対してそれほど脆弱ではなくします。
生き続けることを超えて、食料の備蓄を生かし続けることは、捕食者の主な目的です。草食動物が多い場合は捕食者がより積極的に狩りをし、獲物が少ない場合はよりゆっくりと(またはまったく狩りをしない)ようにする方法を見つけてください。草食動物は動き回るため、他の動物が近くにいるときにのみ食べるという簡単なトリックはうまくいかない可能性があります。それはめったに起こらないため、捕食者は飢えてしまいます。しかし、前のターンの観測を、捕食者オブジェクトに保持されている何らかのデータ構造で追跡し、最近見てきたものに基づいて行動させることができます。