Глава 7Проект:Electronic Life
[…] Въпроса дали Машините могат да мисля […] е толкова важен, колкото и въпроса дали Подводниците могат да плуват.”
В главите: „проект”, аз ще се спра на една нова теория за кратък момент и после ще продължим да работим заедно по програмата. Теорията е незаменима, когато учим програмиране, но ако е придружена от четене и разбиране на не-тривиални програми.
Нашия проект в тази глава е да се изгради виртуална екосистема, един малък свят населен със същества, които се движат наоколо и се борят за оцеляване.
Дефиниция
За да направим тази задача лесно управляема, ние коренно ще опростим концепцията за свят. А именно този свят ще бъде двуизмерна мрежа, където всеки обект заема един квадрат от решетката. Всички обекти получават възможност за вземане на решение за предприемане на някакви действия.
За това ние ще разделим времето и пространството в единици с фиксиран размер: квадрати за пространство и време за обикаляне. Разбира се, това е сурово и неточно приближение. Но нашата симулация е предназначена да бъде забавна, а не точна, така че сме свободни да импровизираме.
Можем да дефинираме този свят с plan масив от strings, който излага мрежата на света с помощта на един характер на квадрат.
edit & run code by clicking itvar plan = ["############################", "# # # o ##", "# #", "# ##### #", "## # # ## #", "### ## # #", "# ### # #", "# #### #", "# ## o #", "# o # o ### #", "# # #", "############################"];
Характера “ # ” в този план представлява стени и скали, а характера “ o ” са живи същества. Интервалите, както се досещате са празни пространства.
Масива plan може да се използва за създаване на обекта свят. Такъв обект следи размера и съдържанието на света. Той има toString
метод, който превърне обратно света в печатащ string (базиран подобно на plan), така че да можем да видим какво се случва вътре. Обекта на света също има метод 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); };
На следващо място, се нуждаем от тип обект, който моделира самата мрежа. Мрежата е част от света, но ние го правим, като отделен обект (който ще бъде свойство на обекта-свят), за да запазим обекта-свят опростен. Света трябва да се самосезира за свързаните със света неща, а мрежата трябва да се занимава със свързаните с мрежата неща.
За да съхраняваме мрежа от стойности, ние имаме няколко опции. Можем да използваме масив с масиви от редове и две свойства, за да имаме достъп до конкретен квадрат, като този:
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) се намира в позиция 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
Тъй като, действителния достъп до този масив ще бъде увит в методите на типа за обекта на мрежа, няма значение за външния код, кой подход ще изберем. Избирам второто представяне, защото прави много по-лесно създаването на масив. Когато извикаме 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
конструктора, ние трябва да направим по-специфични Critter
обекти, които ще живеят в него. Споменах, че света изисква от съществата, които живеят в него да могат да предприемат някакви действия. Това работи по следния начин: всеки обект има метод act
, който когато бъде извикан връща действие. Действието е обект със свойство type
, който има имена за всеки вид действие, коeто съществото иска да предприеме, например "move"
. Действието може да съдържа и допълнителна информация, като например посоката, в която съществото иска да се придвижи.
Съществата са ужасно късогледи и могат да видят само квадратите около тях в мрежата. Но дори и това ограничено зрение може да бъде полезно, когато се решава какво действие да предприемат. Когато се извика метода act
се дава на view обект, който позволява на съществата да инспектират околността около тях. Ние ще назовем осемте околни квадрата с посоки от компаса: "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) };
Обекта view разполага с метод look
, който взема посока и връща харакер, например, ако има стена в тази посока връща "#"
или ако няма нищо там връща " "
(space). Обекта предоставя също удобни методи, като find
и findAll
. И двата вземат характер от картата, като аргумент. Първият връща посоката, в която характера може да се намери в близост до същество или връща null
, ако не съществува такава посока. Вторият връща масив съдържащ всички посоки с този характер. Например ако едно същество седи в ляво (запад) от стената, ще получите ["ne", "e", "se"]
, когато извикате findAll
на обекта view с характер "#"
, като аргумент.
Ето едно просто, глупаво същество, което следва носа си, докато не срещне препятствие и след това се измества в произволна отворена посока:
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
върху масива от имена на посоки. Можем също да използваме Object.keys
за да получим този масив от directions
обекта, който дефинирахме по-рано, но това не дава гаранции за реда, по който свойствата са изброени. В повечето случаи, съвременните JavaScript интерпретатори, ще върнат свойствата в реда, по който са определени, но не винаги е задължително.
В act
метода || "s"
е, за да предотврати this.direction
от получаване на стойност null
, ако съществото по някакъв начин попадне в капан, без празно пространство около него (например, в претъпкан ъгъл от други същества).
World - обект
Сега можем да започнем с типа на World
обекта. Конструктора взема plan
(масив от strings, представляващ мрежата на света, описана
по-рано) и legend
, като аргументи. Легендата е обект, който ни казва, какво означава всеки харакер в картата. Тя съдържа конструктор за всеки характер с изключение на space харакера, който винаги се отнася до стойността 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
за да направи лесно разбирането, какъв е характера на елемента, който първоначално е бил създаден.
Ние се нуждаем от това свойство originChar
при изпълнението на света със toString
метода. Този метод изгражда подобен на карта string от текущото състояние на света, чрез минаване на два вложени цикъла над квадратите на мрежата.
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
обекта, чрез създаване на инстанция въз основа на plan от по-рано в главата и след това извикаме toString
върху него, ще получим string подобен на plan, който се появява вътре.
var world = new World(plan, {"#": Wall, "o": BouncingCritter}); console.log(world.toString()); // → ############################ // # # # o ## // # # // # ##### # // ## # # ## # // ### ## # # // # ### # # // # #### # // # ## o # // # o # o ### # // # # # // ############################
this и неговия обхват
World
конструктора съдържа извикване към forEach
. Интересно е да се отбележи, че вътре функцията подадена към forEach
не е пряко в обхвата на функцията на конструктора. Всяко извикване на функция получава свое собствено this
обвързване, така че this
във вътрешната функция не се отнася към новоизградения обект, към който се отнася външния this
. В действителност, когато функцията не се извиква, като метод, this
ще се отнася към глобалния обект.
Това означава, че не можете да напишете this.grid
, за да получите достъп до мрежата от вътрешността на цикъла. Вместо това, външната функция създава нормална локална променлива grid
, чрез която вътрешната функция получава достъп до мрежата.
Това е грешка в дизайна на JavaScript. За щастие следващата версия на езика предоставя решение на този проблем. Междувременно има заобиколно решение. Един общ модел казва var self = this
и от този момент нататък this
се отнася към self
, който е нормална променлива и по този начин видима за вътрешните функции.
Друго решение е да се използва 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
е резултат от bind
извикване и по този начин има this
обвързан с първия аргумент на bind
със стойността на this
във външната функция (която държи test
обекта).
Повечето стандартни по-високо ниво методи на масиви, като forEach
и map
вземат втори не задължителен аргумент, който може да се използва за предоставяне на this
за повикванията към функцията за итерация. Така че можете да изразите предишния пример по малко по-прост начин.
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
, който извиква функция, която дава такъв аргумент. На пример, тук е метода forEach
на нашия Grid
тип, който призовава дадена функция за всеки елемент в мрежата, който не е 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
метод за world обекта, който дава шанс за действие на съществата. Той ще премине през мрежата използвайки метода forEach
, който току що дефинирахме, търсейки обекти с act
метод. Когато намери един, turn
извиква този метод, за да получи обект за действие и извършва действието ако то е валидно. За сега, само "move"
действията са определени.
Има един потенциален проблем с този подход. Можете ли да го видите? Ако оставим съществата да се движат наоколо, могат да се преместят в квадрат, който все още не сме огледали и ние ще им позволим да се движат отново, когато стигнат до този квадрат. По този начин, ние ще трябва да пазим масив със същества, които вече са миналие по своя ред и да ги игнорираме, когато се появят отново.
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
на мрежата, за да има достъп до правилния 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; } };
Първо, ние просто искаме съществото да действа, подавайки го към обекта за оглед, с който разбира заобикалящия го свят и текущата позиция на съществото в този свят (ние ще дефинираме View
в момента). Метода act
връща действие от всякакъв вид.
Ако вида на действието не е "move"
то се игнорира. Ако е "move"
и има direction
свойство, което се отнася до валидна посока и ако квадрата в тази посока е празен (нула), който съществото да използва и съхраняваме съществото в тази дестинация на квадрата.
Забележете, че letAct
се грижи да игнорира глупости при въвеждане, като дали действието на свойството direction
е валидно или свойството type
има смисъл. Този вид защита при програмиране има смисъл в някои ситуации. Основната причина за това е да валидира входа, идващ от не контролирани източници (като потребител или входящ файл), но също може да бъде полезно да се изолират подсистеми една от друга. В този случай, намерението е съществата да могат да бъдат програмирани небрежно - не трябва да се проверява дали техните предвидени действия имат смисъл. Те просто могат да поискат действие и света ще определи дали да го позволи.
Тези два метода не са част от външния интерфейс на World
обекта. Те са вътрешни детайли. Някои езици предоставят начини изрично да се декларират определени методи и частни свойства, и сигнализират за грешка, когато се опитате да ги използвате извън обекта. 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
определя координатите, които се опитваме да погледнем и ако те са вътре в мрежата, намира характера, съответстващ на елемента, който стои там. За координати извън мрежата, look
просто се преструва, че има стена там, така че ако вие дефинирате свят, който не е ограден, съществата няма да бъдат изкушени да се опитват да преминат границите.
Те се движат
Ние създадохме world обекта по-рано. Сега, след като сме добавили всички необходими методи, трябва да бъде възможно да се направи действително движение на света.
for (var i = 0; i < 5; i++) { world.turn(); console.log(world.toString()); } // → … five turns of moving critters
Просто отпечатване на много копия на картата е доста неприятен начин за наблюдение на един свят. Ето защо пясъчника осигурява animateWorld
функция, която ще управлява света, като екранна анимация с движение по три пъти в секунда, докато не натиснете бутона за спиране.
animateWorld(world); // → … life!
Изпълнението на animateWorld
ще остане загадка за сега, но след като прочетете последните глави на тази книга, които обсъждат интеграцията на JavaScript в уеб браузъри, няма да изглежда толкова загадъчно.
Още форми на живот
Драматично събитие от нашия свят е, ако гледате, когато две същества отскачат едно от друго. Сещате ли се за друга форма на поведение?
Има една, когато съществото се движи по стените. В концептуално отношение, съществото се държи с лявата си ръка (лапа, пипало, каквото има) за стената и я следва. Това се оказва доста сложно да се приложи.
Ние трябва да бъдем в състояние да “ изчислим ” посоките с компас. Тъй като посоките са моделирани с помощта на набор от strings, ние трябва да дефинираме наша собствена операция (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
трябва само да “сканира” околноста около съществото, започвайки от лявата му страна и продължавайки по часовниковата стрелка, докато не открие празен квадрат. След това се придвижва по посока на този празен квадрат.
Нещо което усложнява нещата е, че съществото може да спре по средата на празното пространство, както при стартовата му позиция или като резултат от разходката на друго същество. Ако приложим подход, само с описване на празно пространство, съществото просто ще продължи да завива на ляво и да обикаля в кръг.
Така че има допълнителна проверка if
, за да стартирате сканирането на ляво, само ако съществото току що е преминало някаква пречка - тоест, ако пространството зад и на ляво от съществото не е празно. В противен случай съществото започва да сканира директно напред и ще върви напред, ако пространството отпред е празно.
И накрая има един тест за сравняване на this.dir
и start
след всеки преминал цикъл, за да се уверим, че цикълът няма да работи вечно, ако съществото попадне в претъпкано от други същества място и не може да намери празен квадрат.
Този малък свят демонстрира движението по стените на съществата.
animateWorld(new World( ["############", "# # #", "# ~ ~ #", "# ## #", "# ## o####", "# #", "############"], {"#": Wall, "~": WallFollower, "o": BouncingCritter} ));
Една по-реалистична симулация
За да направим живота в нашия свят по-интересен, ще добавим концепции за храна и възпроизвеждане. Всяко живо същество в света получава ново свойство energy
, което се намалява с извършените действия и се увеличава с яденето на храна. Когато съществото има достатъчно енергия, то може да се възпроизвежда - генерира ново същество от своя вид. За да опростим нещата, съществата в нашия свят ще се размножават безполово, от само себе си.
Ако съществата само се движат и ядат едни други, света скоро ще попадне под закона за увеличаване на ентропията, изчерпване на енергията и ще се превърне в безжизнена пустиня. За да предотвратим това да се случи (твърде бързо) ние ще добавим растения в света. Те не се движат, а просто използват фотосинтеза за да растат (това повишава тяхната енергия) и да се възпроизвеждат.
За да направим това, ще имаме нужда от един свят с различен letAct
метод. Бихме могли просто да заменим метода на прототипа на World
, но аз станах малко привързан към тази симулация с ходещи по стените същества и не ми се иска да прекъсвам този стар свят.
Едно решение е да използваме наследяване. Ще създадем нов конструктор 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
, ако успешно се справи с действието. Обърнете внимание на използването на call
за даване на достъп на манипулатора на света, чрез своя this
.
Ако действието не работи по някаква причина, действието по подразбиране за съществото е просто да чака. Съществото губи една пета от енергията си и ако неговото енергийно ниво падне до или по-ниско от нула, то умира и се отстранява от мрежата.
Обработка на действие
Най -простото действие, което съществата могат да изпълнят е растеж ("grow"
) използвано от растенията. Когато даден вид обект за действие, като {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; };
Възпроизвеждането струва две нива на енергия за новородено същество. Така че, за да създадете (хипотетично) бебе, използваме първо 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);
Повечето време, растенията се размножават и разширяват доста бързо, но след това изобилието от храна предизвиква взрив на населението на тревопасните, които унищожават всички или почти всички растения, което води до масов глад на тревопасните. Понякога екосистемата се възстановява и започва друг цикъл. В друг случай, един от видовете умира напълно. Ако умрат тревопасните, цялото пространство ще се запълни с растения. Ако умрат растенията, останалите същества ще гладуват и долината ще се превърне в безжизнена пустиня. Това е жестокостта на природата.
Упражнения
Изкуствена глупост
Когато жителите на нашия свят изчезнат след няколко минути е доста депресиращо. За да се справят с това, бихме могли да опитаме да създадем едни по-интелигентни тревопасни.
Има няколко очевидни проблема с нашите тревопасни животни. Първо: те имат ужасно голям апетит и изяждат всяко растение, което виждат докато унищожат всички растения. Второ: тяхното произволно движение (спомнете си, че view.find методът връща случайни посоки) ги води в безплодни участъци и те гладуват, ако няма растения на близо. И на края те се размножават много бързо, което прави циклите между изобилие и глад доста интензивни.
Добавете нов вид животно, което се опитва да се справи с една или с повече от тези точки и замени стария тип PlantEater
в света на долината. Вижте как се справя. Модифицирайте го още малко, ако е необходимо.
// Your code here function SmartPlantEater() {} animateWorld(new LifelikeWorld( ["############################", "##### ######", "## *** **##", "# *##** ** O *##", "# *** O ##** *#", "# O ##*** #", "# ##** #", "# O #* #", "#* #** O #", "#*** ##** O **#", "##**** ###*** *###", "############################"], {"#": Wall, "O": SmartPlantEater, "*": Plant} ));
Проблемът с ненаситността може да бъде атакуван по няколко начина. Съществата могат да спрат да ядат, когато достигнат определено ниво на енергия. Или те могат да се хранят само веднъж на всеки N-ти завой (чрез водене на брояч за завоите от последното хранене в свойство на обекта на съществото). Или да се уверим, че растенията никога не изчезват напълно, животните могат да се откажат да ядат растение, ако невидят поне едно друго наблизо (с помощта на findAll
метода на view
). Комбинацията на някои от тези с някои съвсем различни стратегии също може да проработи.
Осъществяването на по-ефективно движение на съществата може да стане с кражба на една от стратегиите за движение в нашия стар energyless свят. Както подскачащото поведение и поведението за стената, показват много по-широк диапазон на движение, отколкото напълно случайни стратегии.
По-бавното размножаване на съществата е тривиално. Просто увеличете минималното ниво на енергия, с което се размножават. Разбира се, когато екосистемата е по-стабилна това я прави скучна. Ако имате една шепа мазни, неподвижни същества в люшкащото се море от растения и те никога не се възпроизвеждат, това прави една много стабилна екосистема. Но никой не иска да гледа това.
Хищници
Всяка сериозна екосистема има хранителна верига по-дълга от едно единствено животно. Напишете друго животно, което оцелява с ядене на тревопасни същества. Ще забележете, че стабилността е още по-трудна за постигане сега, има цикли на няколко нива. Опитайте се да намерите стратегия за живота в екосистемата, да тече гладко поне за известно време.
Едно нещо, което може да ви помогне е да направите света по-голям. По този начин местния бум на населението или унищожението на един вид е по-малко вероятно да се случи и има по голяма плячка за поддържането на популацията на малък хищник.
// 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} ));
Много от същите трикове, които работят в предното упражнение може да се приложат тук. Препоръчвам създаване на големи хищници (с много енергия) и бавно възпроизвеждане. Това ще ги направи по-малко уязвими за големи периоди на глад, когато тревопасните са оскъдни.
Оставането им живи, запазвайки източника на своята храна също жива е основната цел на хищника. Намери някакъв начин да направиш хищниците да ловуват по-агресивно, когато има много тревопасни животни и да ловуват по-бавно (или изобщо), когато плячката е рядкост. Тъй като тревопасните се движат на около е прост трик да се ядат само, когато и други тревопасни са наблизо, което е малко вероятно - това ще се случи толкова рядко, че хищниците ще гладуват. Но може да следите наблюденията от предишните завои в някои структури от данни, съхранени в обекта на хищниците и да базирате поведението им на това, което са видели наскоро.