表現力豊かなJavaScript:電子生活プロジェクト

内容







車が考えることができるかどうかの問題は、潜水艦が航行できるかどうかの問題と同じくらい適切です。



Edsger Dijkstra、コンピューティングサイエンスへの脅威



プロジェクトの章では、理論を投げるのをやめ、プログラムについて一緒に取り組みます。 理論はプログラミングの教育に不可欠ですが、重要なプログラムを読んで理解する必要があります。



私たちのプロジェクトは、移動して生存のために戦う生き物が生息する小さな世界である仮想エコシステムの構築です。



定義



タスクを実行可能にするために、世界の概念を大幅に簡素化します。 つまり、世界は2次元のグリッドになり、各エンティティが1つのセルを占有します。 各ターンで、クリーチャーは何らかのアクションを実行できます。



したがって、時間とスペースを固定サイズの単位に分割します。つまり、スペースのセルと時間の動きです。 もちろん、これは大雑把でずさんな近似です。 しかし、私たちのシミュレーションは、面白くなくて面白くなければならないので、自由に「コーナーをカット」します。



プランの助けを借りて世界を定義することができます-セルごとに1文字を使用して世界グリッドをレイアウトする文字列の配列。



var plan = ["############################", "# # # o ##", "# #", "# ##### #", "## # # ## #", "### ## # #", "# ### # #", "# #### #", "# ## o #", "# o # o ### #", "# # #", "############################"];
      
      







記号「#」は壁と石を意味し、「o」は生物です。 スペースは空のスペースです。



計画を使用して、世界のオブジェクトを作成できます。 彼は世界の規模と内容を監視しています。 これには、世界を出力ライン(それが基にしているプラ​​ンなど)に変換するtoStringメソッドがあり、内部で何が起こっているかを観察できます。 ワールドのオブジェクトにはターンメソッドがあり、すべてのクリーチャーが1つの動きを行い、アクションに従ってワールドの状態を更新できます。



スペースを描く



世界をモデリングするグリッドには幅と高さがあります。 セルは、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
      
      







または、幅×高さのサイズの1つの配列を使用して、要素(x、y)が位置x +(y×width)にあると判断できます。



 var grid = ["top left", "top middle", "top right", "bottom left", "bottom middle", "bottom right"]; console.log(grid[2 + (1 * 3)]); // → bottom right
      
      







アクセスはメッシュオブジェクトのメソッドにラップされるため、外部コードはどちらのアプローチが選択されるかを気にしません。 アレイを作成する方が簡単なので、2番目を選択しました。 1つの数値を引数としてArrayコンストラクターを呼び出すと、指定された長さの新しい空の配列が作成されます。



次のコードは、メインメソッドでグリッドオブジェクトを宣言します。



 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
      
      







クリーチャープログラミングインターフェイス



ワールドのコンストラクターを取り上げる前に、そこに生息するクリーチャーのオブジェクトを決定する必要があります。 私は、世界は生き物に彼らが何をしたいのかを尋ねると述べた。 これは次のように機能します。すべてのクリーチャーオブジェクトには、呼び出されたときにアクションを返すactメソッドがあります。 アクション-タイププロパティのオブジェクト。クリーチャーが実行したいアクションのタイプ、たとえば「移動」を指定します。 アクションには、移動方向などの追加情報が含まれる場合があります。



クリーチャーはひどく近視眼的であり、生き物のすぐ隣にいる細胞しか見えません。 しかし、これはアクションを選択するときに役立ちます。 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メソッドも提供します。 どちらも、引数としてマップ上のものを表す文字の1つを取ります。 最初は、このアイテムがクリーチャーの隣で見つかる方向を返します。そのようなアイテムが近くにない場合はnullを返します。 2番目は、そのようなオブジェクトが見つかったすべての可能な方向を含む配列を返します。 たとえば、壁の左側(西側)のクリーチャーは、引数 "#"を指定してfindAllを呼び出すと、["ne"、 "e"、 "se"]を受け取ります。



これは、障害物に衝突してランダムな方向に跳ね返るまで歩くだけの単純なダム生物です。



 function randomElement(array) { return array[Math.floor(Math.random() * array.length)]; } function BouncingCritter() { this.direction = randomElement(Object.keys(directions)); }; 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コンストラクターはObject.keysを呼び出します。 前の章でこの関数を見ました-オブジェクトのすべてのプロパティ名を含む配列を返します。 ここで、彼女は前に指定したルートオブジェクトからすべてのルート名を取得します。



構造「|| 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では、最初に目的の型のインスタンスを作成し、シンボルコンストラクターを見つけて、それに新しいものを適用します。 次に、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() {}
      
      







Worldオブジェクトを確認し、章の最初で指定したプランを使用してインスタンスを作成し、そのtoStringメソッドを呼び出すと、このプランに非常に似た行が得られます。



 var world = new World(plan, {"#": Wall, "o": BouncingCritter}); console.log(world.toString()); // → ############################ // # # # o ## // # # // # ##### # // ## # # ## # // ### ## # # // # ### # # // # #### # // # ## o # // # o # o ### # // # # # // ############################
      
      







これとその範囲



WorldコンストラクターにはforEachの呼び出しがあります。 forEachに渡された関数内では、コンストラクターのスコープ内に直接いないことに注意してください。 各関数呼び出しは独自の名前空間を取得するため、その内部のこれは、関数の外部を参照する作成されたオブジェクトを参照しなくなります。 一般に、関数がメソッドとして呼び出されない場合、これはグローバルオブジェクトを参照します。



したがって、ループ内からグリッドにアクセスするためにthis.gridを記述することはできません。 代わりに、外部関数は、内部関数がグリッドにアクセスするためのローカルグリッド変数を作成します。



これはJavaScriptデザインの失敗です。 幸いなことに、次のバージョンにはこの問題の解決策があります。 それまでの間、回避策があります。 通常書く



 var self = this
      
      







その後、彼らは自己変数を操作します。



別の解決策はbindメソッドを使用することです。これにより、特定のthisオブジェクトにバインドできます。



 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に渡される関数は呼び出しバインディングの結果であるため、そのthisはbindに渡される最初の引数、つまり外部関数(テストオブジェクトを含む)のthis変数にバインドされます。



forEachやmapなど、配列の標準的な高次メソッドのほとんどは、オプションの2番目の引数を受け入れます。これは、反復関数を呼び出すときにこれを渡すためにも使用できます。 前の例を少し簡単に書くことができます。



 var test = { prop: 10, addPropTo: function(array) { return array.map(function(elt) { return this.prop + elt; }, this); // ←  bind } }; console.log(test.addPropTo([5])); // → [15]
      
      







これは、このようなコンテキストパラメータを持つ高階関数でのみ機能します。 そうでない場合は、前述の他のアプローチを使用する必要があります。



独自の高階関数では、callメソッドを使用して引数として渡された関数を呼び出すコンテキストパラメーターサポートを有効にできます。 たとえば、nullまたは未定義ではない各ラティス要素に対して特定の関数を呼び出すGridタイプのforEachメソッドは次のとおりです。



 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)); } } };
      
      







世界を蘇らせる



次のステップは、クリーチャーが行動できるようにするワールドオブジェクトのターンメソッド(ステップ)を作成することです。 forEachメソッドでグリッドを走査し、actメソッドを持つオブジェクトを探します。 オブジェクトが見つかったら、turnはこのメソッドを呼び出して、アクションオブジェクトを取得し、有効な場合はこのアクションを生成します。 これまでのところ、「移動」アクションのみを理解しています。



考えられる問題が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); };
      
      







forEachメソッドの2番目のパラメーターは、内部関数の正しいthis変数にアクセスするために使用されます。 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; } };
      
      







まず、クリーチャーにアクションを要求するだけで、ワールドとそのクリーチャーのワールド内の現在位置を知るビューオブジェクトを渡します(すぐにビューを設定します)。 actメソッドはアクションを返します。



アクションのタイプが「移動」ではない場合、無視されます。 「移動」し、有効な方向を参照する方向プロパティがあり、この方向のセルが空(null)である場合、クリーチャーが単にnullであったセルを割り当て、クリーチャーを宛先セルに保存します。



letActは無効な入力を無視することに注意してください。 デフォルトでは、方向が有効であることや、タイププロパティが意味をなすとは想定していません。 この種の防御的なプログラミングは、状況によっては理にかなっています。 これは主に、制御していないソース(ユーザー入力またはファイルの読み取り)からの入力をチェックするために行われますが、サブシステムを相互に分離するのにも役立ちます。 私たちの場合、その目的は、クリーチャーが不正確にプログラムされる可能性があることを考慮することです。 彼らの意図が理にかなっているかどうかをチェックする必要はありません。 彼らは単に行動の可能性を要求し、世界自体がそれを許可するかどうかを決定します。



これらの2つのメソッドは、ワールドオブジェクトの外部インターフェイスに属していません。 これらは内部実装の一部です。 一部の言語は、特定のメソッドとプロパティを「プライベート」に宣言し、オブジェクトの外部で使用しようとするとエラーをスローする方法を提供します。 JavaScriptではこれが提供されないため、オブジェクトのインターフェイスの一部を報告するには、他のメソッドに依存する必要があります。 たとえば、アンダースコア(_)などの内部名の特別なプレフィックスを使用して、プロパティの命名スキームを使用して内部と外部を区別すると役立つ場合があります。 これにより、インターフェイスの一部ではないプロパティの偶発的な使用の識別が容易になります。



不足している部分であるViewと入力すると、次のようになります。



 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メソッドは、見ようとしている座標を計算します。 それらがグリッド内にある場合、そこにある要素に対応するシンボルを取得します。 グリッドの外側の座標については、壁のふりをしてください-周囲の壁のない世界を設定すると、クリーチャーは端から出られなくなります。



動く



ワールドオブジェクトのコピーを作成しました。 必要なすべてのメソッドが準備できたので、それを動かすことができるはずです。



 for (var i = 0; i < 5; i++) { world.turn(); console.log(world.toString()); } // → …  
      
      







地図の5つのコピーを表示するだけでは、世界を観察するのにあまり便利な方法ではありません。 したがって、本のサンドボックス(またはダウンロード用のファイル )には魔法の関数animateWorldがあり、ストップを押すまで画面上に世界をアニメーションとして表示し、毎秒3ステップを実行します。



 animateWorld(world); // → … !
      
      







animateWorldの実装は謎のままですが、JavaScriptのブラウザへの統合について説明している本の以下の章を読んだ後、それほど神秘的にはなりません。



より多くの生命体



世界で起こっている興味深い状況の1つは、2つのクリーチャーが互いに跳ね返ったときに起こります。 別の興味深い形のやり取りを思いつくことができますか?



壁に沿って動く生き物を思いつきました。 左手(足、触手など)を壁に保持し、壁に沿って移動します。 結局のところ、これはプログラムするのがそれほど簡単ではありません。



空間内の方向を使用して計算する必要があります。 方向は一連の線で与えられるため、相対方向を計算するには独自のdirPlus操作を設定する必要があります。 dirPlus(「n」、1)は、時計回りに45度北を意味し、「ne」になります。 dirPlus( "s"、-2)は、南から東への反時計回りの回転を意味します。



 var directionNames = Object.keys(directions); 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メソッドは、空のセルが見つかるまで、左側から時計回りにクリーチャーの環境のみをスキャンします。 次に、このセルに向かって移動します。



状況を複雑にしているのは、クリーチャーが空きスペースの壁から遠く離れている可能性があることです。つまり、別のクリーチャーをバイパスするか、最初にそこにいることです。 記載されたアルゴリズムを離れると、不幸なクリーチャーは毎ターン左に曲がり、円を描くように走ります。



そのため、クリーチャーが障害物を通過した直後にスキャンを開始する必要がある場合は、別のチェックが行われます。 つまり、背面と左側のスペースが空でない場合です。 それ以外の場合は、先にスキャンを開始するため、空きスペースでは直進します。



最後に、this.dirの一致をチェックし、サイクルの各パッセージで開始します。これにより、クリーチャーが壁や他のクリーチャーの後ろから行く場所がなく、空のセルが見つからない場合に、サイクルに入らないようにします。



この小さな世界は、壁に沿って動く生物を示しています。



 animateWorld(new World( ["############", "# # #", "# ~ ~ #", "# ## #", "# ## o####", "# #", "############"], {"#": Wall, "~": WallFollower, "o": BouncingCritter} ));
      
      







より多くの生活状況



私たちの小さな世界での生活をより面白くするために、食物と生殖の概念を追加します。 それぞれの生き物にはエネルギーがあり、行動が実行されると減少し、食べ物を食べると増加します。 クリーチャーが十分なエネルギーを持っている場合、それは増殖して同じタイプの新しいクリーチャーを作成できます。 計算を簡素化するために、私たちの生き物は自分で再生します。



生き物がただ動いてお互いを食べると、世界はすぐにエントロピーの増加に屈し、エネルギーはそこで終わり、砂漠に変わります。 この終了(または遅延)を防ぐために、植物を追加します。 彼らは動きません。 彼らは単に光合成に従事し、成長(エネルギーを生成)し、増殖します。



これが機能するためには、異なるletActメソッドを持つ世界が必要です。 ワールドプロトタイプメソッドを置き換えることができますが、私は壁の上を歩く生物のシミュレーションに慣れており、それを破壊したくありません。



1つの解決策は、継承を使用することです。 新しいプロトタイプLifelikeWorldを作成しています。このプロトタイプのプロトタイプはWorldプロトタイプに基づいていますが、letActメソッドをオーバーライドします。 新しい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メソッドは、少なくともいくつかのアクションが渡されたかどうかをチェックし、それを処理する関数があるかどうか、最後にこの関数がtrueを返し、アクションが正常に完了したことを示します。 これを介して関数がワールドオブジェクトにアクセスできるようにするための呼び出しの使用に注意してください。



何らかの理由でアクションが機能しない場合、クリーチャーのデフォルトのアクションは待機です。 彼はエネルギーの0.2単位を失い、エネルギーレベルが0未満に下がると、彼は死に、グリッドから消えます。



アクションハンドラー



最も単純なアクションは成長であり、植物はそれを使用します。 タイプ{type: "grow"}のアクションオブジェクトが返されると、次のハンドラーメソッドが呼び出されます。



 actionTypes.grow = function(critter) { critter.energy += 0.5; return true; };
      
      







成長は常に成功し、植物のエネルギーレベルに半分の単位を追加します。



動きはもっと複雑です。



 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; };
      
      







別のクリーチャーを食べるには、有効な指向性セルの準備も必要です。この場合、セルにはエネルギーのあるもの、たとえば生き物が含まれている必要があります(壁ではなく、食べられません)。これが確認されると、食べた人のエネルギーは食べる人に行き、犠牲者はグリッドから取り除かれます。



最後に、クリーチャーの増殖を許可します。



 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(context) { if (this.energy > 15) { var space = context.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(context) { var space = context.find(" "); if (this.energy > 60 && space) return {type: "reproduce", direction: space}; var plant = context.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} );
      
      







ほとんどの場合、植物は増殖して成長しますが、その後、豊富な食物が草食動物の爆発的な成長につながり、ほとんどすべての植生を食べ尽くし、飢fromからの大量絶滅につながります。時には、生態系が回復し、新しいサイクルが始まります。他の場合には、一部の種が死にます。草食動物なら、空間全体が植物で満たされています。植物-残りの生き物が空腹で死ぬと、谷は無人の荒れ地に変わります。ああ、自然の残酷さ...



演習



人工バカ


私たちの世界の住民が数分で死ぬとき、それは悲しいです。これに対処するために、よりスマートな植物を食べる人を作成することができます。



草食動物にはいくつかの明らかな問題があります。第一に、彼らは貪欲です-彼らはすべての植物を完全に破壊するまで、見つけたすべての植物を食べます。次に、それらのランダムな動き(view.findメソッドがランダムな方向を返すことを思い出してください)は、非効率的にぶらぶらし、近くに植物がなければ飢えで死にます。そして最後に、彼らはあまりにも速く増殖し、それが豊富から飢hungへのサイクルをあまりにも速くします。



1つ以上の問題に対処しようとする新しいタイプのクリーチャーを作成し、それを谷の世界の古いPlantEaterタイプに置き換えます。それらに従ってください。必要な調整を行います。



 //   function SmartPlantEater() {} animateWorld(new LifelikeWorld( ["############################", "##### ######", "## *** **##", "# *##** ** O *##", "# *** O ##** *#", "# O ##*** #", "# ##** #", "# O #* #", "#* #** O #", "#*** ##** O **#", "##**** ###*** *###", "############################"], {"#": Wall, "O": SmartPlantEater, "*": Plant} ));
      
      







捕食者


深刻な生態系では、食物連鎖は1つのリンクよりも長くなります。草食動物を食べて生き残る別の生き物を書きます。サイクルが異なるレベルで発生すると、安定性を達成するのがさらに難しくなることがわかります。エコシステムがしばらくスムーズに実行できるようにする戦略を見つけてください。



世界を拡大するとこれに役立ちます。そうなると、地域の人口爆発や人口減少によって人口が完全に破壊される可能性は低くなり、捕食者の少数の集団を支えることができる比較的大きな獲物の集団の余地があります。



 //    function Tiger() {} animateWorld(new LifelikeWorld( ["####################################################", "# #### **** ###", "# * @ ## ######## OO ##", "# * ## OO **** *#", "# ##* ########## *#", "# ##*** * **** **#", "#* ** # * *** ######### **#", "#* ** # * # * **#", "# ## # O # *** ######", "#* @ # # * O # #", "#* # ###### ** #", "### **** *** ** #", "# O @ O #", "# * ## ## ## ## ### * #", "# ** # * ##### O #", "## ** OO # # *** *** ### ** #", "### # ***** ****#", "####################################################"], {"#": Wall, "@": Tiger, "O": SmartPlantEater, //    "*": Plant} ));
      
      






All Articles