KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » Марейн Хавербеке - Выразительный JavaScript

Марейн Хавербеке - Выразительный JavaScript

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Марейн Хавербеке, "Выразительный jаvascript" бесплатно, без регистрации.
Перейти на страницу:

   "#  ##  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.

Если действие по какой-либо причине не сработало, действием по умолчанию для существа будет ожидание. Он теряет 0,2 единицы энергии, а когда его уровень энергии падает ниже нуля, он умирает и исчезает с сетки.

Обработчики действий

Самое простое действие – рост, его используют растения. Когда возвращается объект action типа {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 на оригинальном существе. Как только у нас есть отпрыск, мы можем выяснить его энергетический уровень и проверить, есть ли у родителя достаточно энергии, чтобы родить его. Также нам потребуется допустимая клетка направления.

Если всё в порядке, отпрыск помещается на сетку (и перестаёт быть гипотетическим), а энергия тратится.

Населяем мир

Теперь у нас есть основа для симуляции существ, больше похожих на настоящие. Мы могли бы поместить в новый мир существ из старого, но они бы просто умерли, так как у них нет свойства energy. Давайте сделаем новых. Сначала напишем растение, которое, по сути, довольно простая форма жизни.

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}

);

Большую часть времени растения размножаются и разрастаются, но затем изобилие еды приводит к взрывному росту популяции травоядных, которые съедают почти всю растительность, что приводит к массовому вымиранию от голода. Иногда экосистема восстанавливается и начинается новый цикл. В других случаях какой-то из видов вымирает. Если травоядные, тогда всё пространство заполняется растениями. Если растения – оставшиеся существа умирают от голода, и долина превращается в необитаемую пустошь. О, жестокость природы…

Упражнения

Искусственный идиот

Грустно, когда жители нашего мира вымирают за несколько минут. Чтобы справиться с этим, мы можем попробовать создать более умного поедателя растений.

У наших травоядных есть несколько очевидных проблем. Во-первых, они жадные – поедают каждое растение, которое находят, пока полностью не уничтожат всю растительность. Во-вторых, их случайное движение (вспомните, что метод view.find возвращает случайное направление) заставляет их болтаться неэффективно и помирать с голоду, если рядом не окажется растений. И наконец, они слишком быстро размножаются, что делает циклы от изобилия к голоду слишком быстрыми.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*