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

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

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

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 вызывает этот метод, получая объект action и производит это действие, если оно допустимо. Пока мы понимаем только действие “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, который знает про мир и текущее положение существа в мире (мы скоро зададим View). Метод act возвращает какое-либо действие.

Если тип действия не “move”, оно игнорируется. Если “move”, и если у него есть свойство direction, ссылающееся на допустимое направление, и если клетка в этом направлении пустует (null), мы назначаем клетке, где только что было существо, null, и сохраняем существо в клетке назначения.

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

Эти два метода не принадлежат к внешнему интерфейсу мирового объекта. Они являются деталями внутренней реализации. Некоторые языки предусматривают способы объявлять определённые методы и свойства «приватными», и выдавать ошибку при попытке их использования снаружи объекта. 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 просто притворяется, что там стена – если вы зададите мир без окружающих стен, существа не смогут сойти с края.

Оно двигается

Мы создали экземпляр мирового объекта. Теперь, когда все необходимые методы готовы, у нас должно получиться заставить его двигаться.

for (var i = 0; i < 5; i++) {

  world.turn();

  console.log(world.toString());

}

// → … пять ходов

Просто выводить пять копий карты – не очень удобный способ наблюдения за миром. Поэтому в песочнице для книги (или в файлах для скачивания) есть волшебная функция animateWorld, которая показывает мир как анимацию на экране, делая по три шага в секунду, пока вы не нажмёте стоп.

animateWorld(world);

// → … заработало!

Реализация animateWorld пока останется тайной, но после прочтения следующих глав книги, обсуждающих интеграцию JavaScript в браузеры, она уже не будет выглядеть так загадочно.

Больше форм жизни

Одна из интересных ситуаций, происходящих в мире, случается, когда два существа отскакивают друг от друга. Можете придумать другую интересную форму взаимодействий?

Я придумал существо, двигающееся по стенке. Оно держит свою левую руку (лапу, щупальце, что угодно) на стене и двигается вдоль неё. Это, как оказалось, не так-то просто запрограммировать.

Нам нужно будет вычислять, используя направления в пространстве. Так как направления заданы набором строк, нам надо задать свою операцию 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 только сканирует окружение существа, начиная с левой стороны и дальше по часовой, пока не находит пустую клетку. Затем он двигается в направлении этой клетки.

Усложняет ситуацию то, что существо может оказаться вдали от стен на пустом пространстве — либо обходя другое существо, либо изначально оказавшись там. Если мы оставим описанный алгоритм, несчастное существо будет каждый ход поворачивать налево, и бегать по кругу.

Так что есть ещё одна проверка через if, что сканирование нужно начинать, если существо только что прошло мимо какого-либо препятствия. То есть, если пространство сзади и слева не пустое. В противном случае сканировать начинаем впереди, поэтому в пустом пространстве он будет идти прямо.

И наконец, есть проверка на совпадение this.dir и start на каждом проходе цикла, чтобы он не зациклился, когда существу некуда идти из-за стен или других существ, и оно не может найти пустую клетку.

Этот небольшой мир показывает существ, двигающихся по стенам:

animateWorld(new World(

  ["############",

   "#     #    #",

   "#   ~    ~ #",

   "#  ##      #",

   "#  ##  o####",

   "#          #",

   "############"],

  {"#": Wall,

   "~": WallFollower,

   "o": BouncingCritter}

));

Более жизненная ситуация

Чтобы сделать жизнь в нашем мирке более интересной, добавим понятия еды и размножения. У каждого живого существа появляется новое свойство, energy (энергия), которая уменьшается при совершении действий, и увеличивается при поедании еды. Когда у существа достаточно энергии, он может размножаться, создавая новое существо того же типа. Для упрощения расчётов наши существа размножаются сами по себе.

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