diff --git a/README.md b/README.md
index 757dd16..4717b02 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,772 @@
-# js-game
-Дипломный проект по курсу JavaScript
+Дипломный проект курса JavaScript
+===
+
+В рамках дипломного проекта вам необходимо реализовать ключевые компоненты игры. Игра будет запускаться и работать в браузере.
+
+## Реализация
+
+Реализовывать проект и предоставить его на проверку можно двумя способами:
++ Локально и публиковать код в ваш репозитории [GitHub] или [BitBucket]
++ В онлайн песочнице [CodePen] или [Repl.it]
+
+Сама игра будет функционировать когда вы окончательно реализуете все компоненты. Но чтобы понять правильно ли реализован каждый из них, для каждого компоненты дан пример кода и результат работы его, по которому вы можете проверить правильно ли вы его реализовали. Сам код пимеров в итоговом решении оставлять не рекомендуется.
+
+Так же есть возможность запустить тесты, которые покажут верно ли реализован каждый компонент. Об этом будет подробно описано в разделе Тестирование.
+
+### Реализация в репозитории
+
+#### Подготовка репозитория
+
+1. Установить git.
+2. Создайте аккаунт в сервисе [GitHub] или [BitBucket]
+3. Создайте публичный репозиторий.
+4. Скопируйте ссылку на репозиторий (рекомендуем использовать HTTPS, если ранее вы не сталкивались с SSH).
+5. Клонируйте ваш репозиторий локально используя команду `git clone`.
+
+Итогом будет наличие папки на локальном компьютере, в которым инициализорован git репозиторий, и настроена связь с репозиторием на [GitHub] или [BitBucket].
+
+#### Подготовка проекта
+
+1. Скачайте свежую версию проекта по ссылке
+
+ https://github.com/netology-code/js-game/releases
+
+2. Разверните архив проекта в папку созданную при подготовке репозитория.
+3. Ваш код пишите в файле `./game.js`
+4. Для запуска игры откройте в браузере файл `./index.html`
+5. Для запуска тестов откройте в браузере файл `./test/index.html`
+
+Менять остальные файлы не рекомендуется.
+
+#### Публикация промежуточных версий
+
+1. Добавьте к коммиту файл `game.js` командой `git add game.js`
+2. Сделайте коммит `git commit`
+3. Опубликуйте изменения с помощью команды `git push`
+
+#### Создание локального сервера (не обязательно)
+
+Все компоненты игры будут работать локально, кроме функции `loadLevels`, действия которой будут заблокированы политикой безопасности бразуера.
+
+Один из вариантов обойти это: запустить локальный веб-сервер.
+
+##### Локальный сервер на php
+
+1. Установить php на компьютер
+2. Для запуска сервера в папке проекта запустить команду `php -S localhost:3000`
+3. Для запуска игры откройте в браузере адрес `http://localhost:3000/index.html`
+4. Для запуска тестов откройте в браузере адрес `http://localhost:3000/test/index.html`
+
+##### Локальный сервер на NodeJS
+
+1. Установить NodeJS
+2. В папке проекта выполнить команду `npm install`
+3. Для запуска сервера в папке проекта запустить команду `npm start`
+4. Для запуска игры откройте в браузере адрес `http://localhost:3000/index.html`
+5. Для запуска тестов откройте в браузере адрес `http://localhost:3000/test/index.html`
+
+При использовании NodeJS тесты и игра будут обновляться автоматически при изменении файлов.
+
+
+### Реализация в песочнице
+
+#### CodePen
+
+Для реализации в онлайн песочнице вам нужно:
+
+1. Зарегистрироваться на сервисе [CodePen]
+2. Открыть заготовку проекта по ссылке:
+
+ https://codepen.io/dfitiskin/pen/XRZqWd?editors=0010
+
+3. Нажать кнопку «Fork», тем самым создав свою копию заготовки.
+4. Реализовывать код игры последовательно следую инструкции в окне «JS».
+5. Переодически сохраняйте результат, чтобы не потерять изменения.
+6. Отправляйте наставнику на проверку ссылку на ваш пен.
+
+Инструкция по использованию сервиса CodePen:
+https://netology-university.bitbucket.io/wm/resourses/codepen-guide.html
+
+#### Repl.it
+
+1. Зарегистрироваться на сервисе [Repl.it]
+2. Создать новую песочницу «HTML, CSS, JS»
+
+ https://repl.it/languages/web_project
+
+3. Во вкладке `index.html` поместите следующий код
+
+ ```html
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+ ```
+
+4. Нажмите кнопку «Save»
+5. Реализовывать код игры последовательно следую инструкции во вкладке «index.js».
+6. Переодически сохраняйте результат, чтобы не потерять изменения.
+7. Отправляйте наставнику на проверку ссылку на вашу песочницу, которую пожно получить по кнопке «Share».
+
+## Тестирование
+
+В файле `./test/index.html` настроена среда автоматизированного тестирования вашего кода. Она проверяет созданные компоненты на соответствие требованиям. И если находит расхождения, сообщает об ошибки. Тем самым, тесты — ваш навигатор, показывающий какой часть требований в вашем коде вы выполнили, а какую нет.
+
+По тестам можно осуществлять навигацию. Можно выбрать конкретный компонет, или конкретный метод, и следить за выполнением только выбранных тестов, не отвлекаясь на другие.
+
+Так же можно отобразить только проваленные тесты, или наоборот, только успешные.
+
+Просто кликайте на соответствующий пункт, чтобы сосредоточиться на нем.
+
+Процесс реализации можно построить таким образом:
+1. Выбрать компонет или даже метод компонента.
+2. Отфильтровать тесты, оставив только выбранный компонент или его метод.
+3. Реализовать код который удовлетворит первому проваленному тесту.
+4. Убедиться что тест помечен как успешный.
+5. Если остались еще проваленные тесты, вернуться к пункту 3.
+
+Такой подход называется разработка через тестирование или TDD. За тем лишь исключением, что тесты уже написаны.
+
+## Процесс и порядок реализации
+
+Для того чтобы максимально просто и быстро получить базовый рабочий вариант проекта, рекомендуем придерживаться следующего плана разработки:
+
+1. Реализовать базовые классы игры `Vector`, `Actor` и `Level`.
+2. После этого вы уже сможете запустить игру.
+ ```javascript
+ const grid = [
+ new Array(3),
+ ['wall', 'wall', 'lava']
+ ];
+ const level = new Level(grid);
+ runLevel(level, DOMDisplay);
+ ```
+
+ На экране отобразится схема уровня. Узнайте подробнее про функцию `runLevel` и класс `DOMDisplay` ниже.
+
+3. Реализуйте `LevelParser`, что позволит вам описывать уровни с помощью текстовой схемы:
+ ```javascript
+ const schema = [
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' !xxx',
+ ' ',
+ 'xxx! ',
+ ' '
+ ];
+ const parser = new LevelParser();
+ const level = parser.parse(schema);
+ runLevel(level, DOMDisplay);
+ ```
+4. Реализуйте `Player` и поместите его символ на схему и добавьте словарь при создании парсера:
+ ```javascript
+ const schema = [
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' !xxx',
+ ' @ ',
+ 'xxx! ',
+ ' '
+ ];
+ const actorDict = {
+ '@': Player
+ }
+ const parser = new LevelParser(actorDict);
+ const level = parser.parse(schema);
+ runLevel(level, DOMDisplay);
+ ```
+5. Реализуйте другие движущиеся объекты игрового поля и помещайте их символы на схему и в словарь парсера.
+6. Реализуйте загрузку уровней с помощью функции `loadLevels` и запуск игры с помощью `runGame`.
+7. Когда игрок пройдет все уровни, используйте функцию `alert` чтобы сообщить о победе.
+
+## Компоненты которые реализованы и их необходимо использовать
+
+### Класс `DOMDisplay`
+
+Отвечает за отрисовку в браузере сетки игрового поля и движущихся объектов. Конструктор принимает два аргумента: первый узел DOM, в котором необходимо отрисовать игровое поле и уровень `Level` описывающий игровое поле.
+
+Непосредственно создавать этот объект не потребуется. Его необходимо передавать вторым аргументом в функцию `runLevel` и третьим аргументом в функцию `runGame`.
+
+Пример использования:
+```javascript
+const schema = [
+ ' ',
+ ' ',
+ ' = ',
+ ' ',
+ ' !xxx',
+ ' @ ',
+ 'xxx! ',
+ ' '
+];
+const actorDict = {
+ '@': Player,
+ '=': HorizontalFireball
+}
+const parser = new LevelParser(actorDict);
+const level = parser.parse(schema);
+DOMDisplay(document.body, level);
+```
+
+После такого вызова будет отрисовано исходное состояние сетки игрового поля, все движущиеся объекты. Но эта схема будет статичной.
+
+### Функция `runLevel`
+
+Инициализирует процесс регулярной отрисовки текущего состояния игрового поля и обработку событий клавиатуры.
+
+Принимает два аргумента: уровень, объект класса `Level` и конструктор объекта отвечающего за отрисовку. В случае реализации игры в браузере вторым аргументом необходимо использовать класс `DOMDisplay`.
+
+Функция возвращает промис, который разрешится статусом завершения игры, _строка_. С учетом реализации класс `Level` он может принимать значений `won` и `lost`.
+
+Пример использования:
+```javascript
+const schema = [
+ ' ',
+ ' ',
+ ' = ',
+ ' o ',
+ ' !xxx',
+ ' @ ',
+ 'xxx! ',
+ ' '
+];
+const actorDict = {
+ '@': Player,
+ '=': HorizontalFireball
+}
+const parser = new LevelParser(actorDict);
+const level = parser.parse(schema);
+runLevel(level, DOMDisplay)
+ .then(status => console.log(`Игрок ${status}`));
+```
+
+После вызова такого кода в браузере будет отрисована схема уровня, движущиеся объекты будут перемещаться, и вы сможете управлять игроком с клавиатуры.
+
+### Функция `runGame`
+
+Инициализирует процесс прохождения игры состоящей из последовательного прохождения нескольких уровней.
+
+Принимает три аргумента: список схем уровней, _массив_ каждый элемент которого схема (массив строк); парсер схем, _объект_ `LevelParser`, и конструктор объекта отвечающего за отрисовку. В случае реализации игры в браузере третьим аргументом необходимо использовать класс `DOMDisplay`.
+
+Возвращает промис, который разрешится когда пользователь пройдет все уровни.
+
+Пример использования:
+```javascript
+const schemas = [
+ [
+ ' ',
+ ' ',
+ ' = ',
+ ' o ',
+ ' !xxx',
+ ' @ ',
+ 'xxx! ',
+ ' '
+ ],
+ [
+ ' v ',
+ ' v ',
+ ' v ',
+ ' o',
+ ' x',
+ '@ x ',
+ 'x ',
+ ' '
+ ]
+];
+const actorDict = {
+ '@': Player,
+ 'v': FireRain
+}
+const parser = new LevelParser(actorDict);
+runGame(schemas, parser, DOMDisplay)
+ .then(() => console.log('Вы выиграли приз!'));
+```
+
+Запустит игру из двух уровней, которые необходимо будет пройти последовательно.
+
+### Функция `loadLevels`
+
+Загружает коллекцию уровней. Не принимает аргументов.
+
+Возвращает промис, который разрешится
+
+## Компоненты которые необходимо реализовать
+
+### Вектор
+
+Необходимо реализовать класс `Vector`, который позволит контролировать расположение объектов в двухмерном пространстве и управлять их размером и перемещением.
+
+#### Контструктор
+
+Принимает два аргумента: координата по оси X и по оси Y, _числа_, по умолчанию `0`.
+
+Создает объект со свойствами `x` и `y` равные переданным в конструктор координатам.
+
+#### Метод `plus`
+
+Принимает один аргумент: вектор, _объект_ `Vector`.
+
+Если передать аргумент другого типа, то бросает исключение `Можно прибавлять к вектору только вектор типа Vector`.
+
+Создает и возвращает новый _объект_ типа `Vector`, координаты которого будут суммой соответствующих координат суммируемых векторов.
+
+#### Метод `times`
+
+Принимает один аргумент: множитель, _число_.
+
+Создает и возвращает новый _объект_ типа `Vector`, координаты которого будут равны соответствующим координатам исходного вектора умноженным на множитель.
+
+#### Пример кода
+```javascript
+const start = new Vector(30, 50);
+const moveTo = new Vector(5, 10);
+const finish = start.plus(moveTo.times(2));
+
+console.log(`Исходное расположение: ${start.x}:${start.y}`);
+console.log(`Текущее расположение: ${finish.x}:${finish.y}`);
+```
+
+Результат выполнения кода:
+```
+Исходное расположение: 30:50
+Текущее расположение: 40:70
+```
+
+### Движущийся объект
+
+Необходимо реализовать класс `Actor`, который позволит контролировать все движущиеся объекты на игровом поле и контролировать их пересечение.
+
+#### Конструктор
+
+Принимает три аргумента: расположение, _объект_ типа `Vector`, размер, тоже _объект_ типа `Vector` и скорость, тоже _объект_ типа `Vector`. Все аргументы не обязательные. По умолчанию создается объект с координатами 0:0, размером на 1x1 и скоростью 0:0.
+
+Если в качестве первого, второго или третьего аргумента передать не объект типа `Vector`, то конструктор должен бросить исключение.
+
+Должно быть определено свойство `pos`, в котором размещен `Vector`.
+
+Должно быть определено свойство `size`, в котором размещен `Vector`.
+
+Должно быть определено свойство `speed`, в котором размещен `Vector`.
+
+Должен быть определен метод `act`, который ничего не делает.
+
+Должны быть определены свойства только для чтения `left`, `top`, `right`, `bottom`, в котором установлены границы объекта по осям X и Y с учетом его расположения и размера.
+
+Должен иметь свойство `type`, строка со значением `actor`, только для чтения.
+
+#### Метод `isIntersect`
+
+Метод проверяет пересекается ли текущий объект с переданным объектом, и если да, возвращает `true`, иначе `false`.
+
+Принимает один аргумент: движущийся объект типа `Actor`. Если передать аргумент другого типа или вызвать без аргументов, то метод бросает исключение.
+
+Если передать в качестве аргумента этот же объект, то всегда возвращает `false`. Объект не пересекается сам с собой.
+
+Объекты имеющие смежные границы не пересекаются.
+
+#### Пример кода
+```javascript
+const items = new Map();
+const player = new Actor();
+items.set('Игрок', player);
+items.set('Первая монета', new Actor(new Vector(10, 10)));
+items.set('Вторая монета', new Actor(new Vector(15, 5)));
+
+function position(item) {
+ return ['left', 'top', 'right', 'bottom']
+ .map(side => `${side}: ${item[side]}`)
+ .join(', ');
+}
+
+function movePlayer(x, y) {
+ player.pos = player.pos.plus(new Vector(x, y));
+}
+
+function status(item, title) {
+ console.log(`${title}: ${position(item)}`);
+ if (player.isIntersect(item)) {
+ console.log(`Игрок подобрал ${title}`);
+ }
+}
+
+items.forEach(status);
+movePlayer(10, 10);
+items.forEach(status);
+movePlayer(5, -5);
+items.forEach(status);
+```
+
+Результат работы примера:
+```
+Игрок: left: 0, top: 0, right: 1, bottom: 1
+Первая монета: left: 10, top: 10, right: 11, bottom: 11
+Вторая монета: left: 15, top: 5, right: 16, bottom: 6
+Игрок: left: 10, top: 10, right: 11, bottom: 11
+Первая монета: left: 10, top: 10, right: 11, bottom: 11
+Игрок подобрал Первая монета
+Вторая монета: left: 15, top: 5, right: 16, bottom: 6
+Игрок: left: 15, top: 5, right: 16, bottom: 6
+Первая монета: left: 10, top: 10, right: 11, bottom: 11
+Вторая монета: left: 15, top: 5, right: 16, bottom: 6
+Игрок подобрал Вторая монета
+```
+
+### Игровое поле
+
+Объекты класса `Level` реализуют схему игрового поля конкретного уровня, контроллируют все движущиеся объекты на нём и реализуют логику игры. Уровень представляет собой координатное поле, имеющее фиксированную ширину и высоту.
+
+Сетка уровня представляет собой координатное двумерное поле, представленное двумерным массивом. Первый массив — строки игрового поля, индекс этого массива соответствует координате Y на игровом поле. Элемент с индексом `5` соответствует строке с координатой Y равной `5`. Вложенные массивы расположенные в элементах массива строк представляют ячейки поля. Индекс этих массивов соответствует координате X. Например, элемент с индексом `10`, соответстует ячейке с координатой X равной `10`.
+
+Так как `grid` это двумерный массив представляющий сетку игрового поля, то чтобы узнать что находится в ячейке с координатами X=10 и Y=5 (10:5), необходимо получить значение `grid[5][10]`. Если значение этого элемента равно `undefined`, то эта ячейка пуста. Иначе там будет строка описывающая препятствие. Например `wall` — для стены и `lava` — для лавы. Отсюда вытекает следующий факт: все препятствия имеют целочисленные размеры и координаты.
+
+#### Конструктор
+
+Принимает два аргумента: сетку игрового поля с препятствиями, _массив массивов строк_, и список движущихся объектов, _массив объектов_ `Actor`. Оба аргумента не обязательные.
+
+Имеет свойство `grid` — сетка игрового поля. Двумерный массив строк.
+
+Имеет свойство `actors` — список движущихся объектов игрового поля, массив объектов `Actor`.
+
+Имеет свойство `height` — высота игрового поля, равное числу строк в сетке из первого аргмента.
+
+Имеет свойство `width` - ширина игрового поля, равное числу ячеек в строке сетки из первого аргмента. При этом, если в разных строках разное число ячеек, то `width` будет равно максимальному количеству ячеек в строке.
+
+Имеет свойство `status` - состояние прохождения уровня, равное `null` после создания.
+
+Имеет свойство `finishDelay` — таймаут после окончания игры, равен `1` после создания. Необходим чтобы после выигрыша или проигрыша игра не завершалась мнгновенно.
+
+#### Метод `isFinished`
+
+Определеяет завершен ли уровень. Не принимает аргументов.
+
+Возвращает `true` если свойство `status` не равно `null` и `finishDelay` меньше нуля.
+
+#### Метод `actorAt`
+
+Определяет расположен ли какой-то другой движущийся объект в переданной позиции, и если да, вернёт этот объект.
+
+Принимает один аргумент — движущийся объект, `Actor`. Если не передать аргумент, или передать не объект `Actor` метод должен бросить исключение.
+
+Возвращает `undefined` если переданный движущийся объект не пересекается ни с одним объектом на игровом поле.
+
+Возвращает объект `Actor`, если переданный объект пересекается с ним на игровом поле. Если пересекается с несколькими объектами, вернет первый.
+
+#### Метод `obstacleAt`
+
+Аналогично методу `actorAt` определяет нет ли препятствия в указанном месте. Так же этот метод контроллирует выход объекта за границы игрового поля.
+
+Так как движущиеся объекты не могут двигаться сквозь стены, то метод принимает два аргумента: положение, куда собираемся передвинуть объект, _вектор_ `Vector`, и размер этого объекта, тоже _вектор_ `Vector`. Если первым и вторым аргументом передать не `Vector`, то метод бросает исключение.
+
+Вернет строку соответствующую препятствию из сетки игрового поля, пересаекающему область описанную двумя переданными векторами, либо `undefined`, если в этой области препятствий нет.
+
+Если описанная двумя векторами область выходит за пределы игрового поля, то метод вернет строку `lava`, если область выступает снизу. И вернет `wall` в остальных случаях. Будем считать, что игровое поле слева, сверху и справа огорожено стеной и снизу у него смертельная лава.
+
+#### Метод `removeActor`
+
+Метод удаляет переданный объект с игрового поля. Если такого объекта на игровом поле нет, не делает ничего.
+
+Принимает один аргумент, объект `Actor`. Находит и удаляет его.
+
+#### Метод `noMoreActors`
+
+Определяет остались ли еще объекты переданного типа на игровом поле.
+
+Принимает один аргумент, тип движущегося объекта, _строка_.
+
+Возвращает `true`, если на игровом поле нет объектов этого типа (свойство `type`). Иначе возвращает `false`.
+
+#### Метод `playerTouched`
+
+Один из ключевых методов, определяющий логику игры. Меняет состояние игрового поля при касании игрока каких-либо объектов или препятствий.
+
+Если состояние игры уже отлично от `null`, то не делаем ничего, игра уже и так завершилась.
+
+Принимает два аргмента. Тип препятствия или объекта, _строка_. Движущийся объект, которого коснулся игрок, объект типа `Actor`, не обязательный аргумент.
+
+Если первым аргументом передать строку `lava` или `fireball`, то меняем статус игры на `lost` (свойство `status`). Игрок проигрывает при касании лавы или шаровой молнии.
+
+Если первым аргментом передать строку `coin`, а вторым объект монеты, то необходимо удалить эту монету с игрового поля. Если при этом на игровом поле не осталось больше монет, то меняем статус игры на `won`. Игрок побеждает, когда собирает все монеты на уровне. Отсюда вытекает факт, что уровень без монет пройти невозможно.
+
+#### Пример кода
+
+```javascript
+const grid = [
+ [undefined, undefined],
+ ['wall', 'wall']
+];
+
+const goldCoin = { type: 'coin', title: 'Золото' };
+const bronzeCoin = { type: 'coin', title: 'Бронза' };
+const player = new Actor();
+const fireball = new Actor();
+
+const level = new Level(undefined, [ goldCoin, bronzeCoin, player, fireball ]);
+
+level.playerTouched('coin', goldCoin);
+level.playerTouched('coin', bronzeCoin);
+
+if (level.noMoreActors('coin')) {
+ console.log('Все монеты собраны');
+ console.log(`Статус игры: ${level.status}`);
+}
+
+const obstacle = level.obstacleAt(new Vector(1, 1), player.size);
+if (obstacle) {
+ console.log(`На пути препятствие: ${obstacle}`);
+}
+
+const otherActor = level.actorAt(player);
+if (otherActor === fireball) {
+ console.log('Пользователь столкнулся с шаровой молнией');
+}
+```
+
+Результат выполнения:
+```
+Все монеты собраны
+Статус игры: won
+На пути препятствие: wall
+Пользователь столкнулся с шаровой молнией
+```
+
+### Парсер уровня
+
+Объект класса `LevelParser` позволяет создать игровое поле `Level` из массива строк по следующему принципу:
+* Каждый элемент массив соответствует строке в сетке уровня.
+* Каждый символ строки соответствует ячейке в сетке уровня.
+* Символ определяет тип объекта или препятствия.
+* Индекс строки, и индекс символа определяют исходные координаты объекта или координаты препятствия.
+
+Символы и соответствующие им препятствия и объекты игрового поля:
+* **x** — стена, препятствие
+* **!** — лава, препятствие
+* **@** — игрок, объект
+* **o** — монетка, объект
+* **=** — движущаяся горизонтально шаровая молния, объект
+* **|** — движущаяся вертикально шаровая молния, объект
+* **v** — огненный дождь, объект
+
+> Обратите внимание что тут слово символ означает букву, цифру или знак, которые используются в строках, а не тип данных `Symbol`.
+
+#### Конструктор
+
+Принимает один аргумент: словарь движущихся объектов игрового поля, _объект_, ключами которого являются символы из текстового представления уровня, а значениями конструкторы, с помощью которых можно создать новый объект.
+
+#### Метод `actorFromSymbol`
+
+Принимает символ, _строка_. Возвращает конструктор объекта по его символу, используя словарь. Если в словаре не нашлось ключа с таким символом, вернет `undefined`.
+
+#### Метод `obstacleFromSymbol`
+
+Аналогично принимает символ, _строка_. Возвращает строку соответствующую символу припятствия. Если символу нет соответствующего препятствия, то вернет `undefined`.
+
+Вернет `wall`, если передать `x`.
+
+Вернет `lava`, если передать `!`.
+
+Вернет `undefined`, если передать любой другой символ.
+
+#### Метод `createGrid`
+
+Принимает массив строк и преобразует его в массив массивов, в ячейках которого хранится либо строка соответствующая препятствию, либо `undefined`.
+
+Движущиейся объекты не должны присутствовать на сетке.
+
+#### Метод `createActors`
+
+Принимает массив строк и преобразует его в массив движущихся объектов используя для их создания конструкторы из словаря.
+
+Количество движущихся объектов в результирующем массиве должно быть равно количеству символов объектов в массиве строк.
+
+Каждый объект должен быть создан с использованием вектора определяющего его положение с учетом координат, полученных на основе индекса строки в массиве (Y) и индекса символа в строке (X).
+
+Для создания объекта должен быть использован конструктор из словаря, соответствующий символу. При этом, если этот конструктор не является экземпляром `Actor`, то такой символ игнорируется, и объект не создается.
+
+#### Метод `parse`
+
+Принимает массив строк, создает и возвращает игровое поле заполненное препятсвиями и движущимися объектами полученными на освнове символов и словаря.
+
+#### Пример использования
+
+```javascript
+const plan = [
+ ' @ ',
+ 'x!x'
+];
+
+const actorsDict = Object.create(null);
+actorsDict['@'] = Actor;
+
+const parser = new LevelParser(actorsDict);
+const level = parser.parse(plan);
+
+level.grid.forEach((line, y) => {
+ line.forEach((cell, x) => console.log(`(${x}:${y}) ${cell}`));
+});
+
+level.actors.forEach(actor => console.log(`(${actor.pos.x}:${actor.pos.y}) ${actor.type}`));
+```
+
+Результат выполнения кода:
+```
+(0:0) undefined
+(1:0) undefined
+(2:0) undefined
+(0:1) wall
+(1:1) lava
+(2:1) wall
+(1:0) actor
+```
+
+### Шаровая молния
+
+Класс `Fireball` станет прототипом для движущихся опасностей на игровом поле. Он должен наследовать весь функционал движущегося объекта `Actor`.
+
+#### Конструктор
+
+Принимает два аргумента: координаты, _объект_ `Vector` и скорость, тоже _объект_ `Vector`. Оба аргумента не обязательные. По умолчанию создается объект с координатами `0:0` и скоростью `0:0`.
+
+Созданный объект должен иметь свойство `type` со значением `fireball`. Это свойство только для чтения.
+
+Так же должен иметь размер `1:1` в свойстве `size`, _объект_ `Vector`.
+
+#### Метод `getNextPosition`
+
+Создает и возвращает вектор `Vector` следующей позиции шаровой молнии. Это функция времени. И как в школьной задаче, новая позиция это текущая позиция плюс скорость умноженная на время. И так по каждой из осей.
+
+Принимает один аргумент, время, _число_. Аргумент не обязательный, по умолчанию равен `1`.
+
+#### Метод `handleObstacle`
+
+Обрабатывает сталкновение молнии с препятствием. Не принимает аргументов. Ничего не возвращает.
+
+Меняет вектор скорости на противоположный. Если он был `5:5`, то после должен стать `-5:-5`.
+
+#### Метод `act`
+
+Обновляет состояние движущегося объекта.
+
+Принимает два аргумента. Первый — время, _число_, второй, игровое поле, _объект_ `Level`.
+
+Метод ничего не возвращает. Но должен выполнить следующие действия:
+1. Получить следующую позицию, используя время.
+2. Выяснить не пересечется ли в следующей позиции объект с каким-либо препятствием. Пересечения с другими движущимися объектами учитывать не нужно.
+3. Если нет, обновить текущую позицию объекта.
+4. Если объект пересекается с препятствием, то необходим обработать это событие. При этом текущее положение остается прежним.
+
+#### Пример использования
+
+```javascript
+const time = 5;
+const speed = new Vector(1, 0);
+const position = new Vector(5, 5);
+
+const ball = new Fireball(position, speed);
+
+const nextPosition = ball.getNextPosition(time);
+console.log(`Новая позиция: ${nextPosition.x}: ${nextPosition.y}`);
+
+ball.handleObstacle();
+console.log(`Текущая скорость: ${ball.speed.x}: ${ball.speed.y}`);
+```
+
+Результат работы кода:
+```
+Новая позиция: 10: 5
+Текущая скорость: -1: 0
+```
+
+### Горизонтальная шаровая молния
+
+Вам необходимо самостоятельно реализовать класс `HorizontalFireball`. Он будет представлять собой объект который движется по горизонтали со скоростью `2` и при столкновении с препятствием движется в обратную сторону.
+
+Конструктор должен принимать один аргумент: координаты текущего положения, _объект_ `Vector`. И создавать объект размером `1:1` и скоростью равной `2` по оси X.
+
+### Вертикальная шаровая молния
+
+Вам необходимо самостоятельно реализовать класс `VerticalFireball`. Он будет представлять собой объект который движется по вертикали со скоростью `2` и при столкновении с препятствием движется в обратную сторону.
+
+Конструктор должен принимать один аргумент: координаты текущего положения, _объект_ `Vector`. И создавать объект размером `1:1` и скоростью равной `2` по оси Y.
+
+### Огненный дождь
+
+Вам необходимо самостоятельно реализовать класс `FireRain`. Он будет представлять собой объект который движется по вертикали со скоростью `3` и при столкновении с препятствием начинает движение в том же направлении из исходного положения, которое задано при создании.
+
+Конструктор должен принимать один аргумент: координаты текущего положения, _объект_ `Vector`. И создавать объект размером `1:1` и скоростью равной `3` по оси Y.
+
+### Монета
+
+Класс `Coin` реализует поведение монетки на игровом поле. Чтобы привлекать к себе внимание, монетки должны постоянно подпрыгивать в рамках своей ячейки. Он должен наследовать весь функционал движущегося объекта `Actor`.
+
+#### Конструктор
+
+Принимает один аргмент: координаты положения на игровом поле, _объект_ `Vector`.
+
+Созданный объект должен иметь размер `0,6:0,6`. А его реальный координаты должны отличаться от тех что переданы в конструктор на вектор `0,2:0,1`.
+
+Свойство `type` созданного объекта должно иметь значение `coin`.
+
+Так же объект должен иметь следующие свойства:
+* Скорость подпрыгивания, `springSpeed`, равное `8`;
+* Радиус подпрыгивания, `springDist`, равен `0.07`;
+* Фаза подпрыгивания, `spring`, случайное число от `0` до `2π`.
+
+#### Метод `updateSpring`
+
+Обновляет фазу подпрыгивания. Это функция времени.
+
+Принимает один аргумент, время, _число_, по умолчанию `1`.
+
+Ничего не возвращает. Обновляет текущую фазу `spring`, увеличив её на скорость `springSpeed` умноженную на время.
+
+#### Метод `getSpringVector`
+
+Создает и возвращает вектор подпрыгивания. Не принимает аргментов.
+
+Так как подпрыгивание происходит только по оси Y, то координата X вектора всегда равна нулю.
+
+Координата Y вектора равна синусу текущей фазы, умноженному на радиус.
+
+#### Метод `getNextPosition`
+
+Обновляет текущую фазу, создает и возвращает вектор новой позиции монетки.
+
+Принимает один аргумент, время, _число_, по умолчанию `1`.
+
+Новый вектор равен базовому вектору положения, увеличенному на вектор подпрыгивания. Увеличивать нужно именно базовый вектор положения, который получен в конструкторе, а не текущий.
+
+#### Метод `act`
+
+Принимает один аргмент, время. Получает новую позицию объекта, и задает её как текущую. Ничего не возвращает.
+
+### Игрок
+
+Класс `Player` содержит базовый функционал движущегося объекта, который представляет игрока на игровом поле. Должен наследовать возможности `Actor`.
+
+#### Конструктор
+
+Принимает один аргумент: координаты положения на игровом поле, _объект_ `Vector`.
+
+Созданный объект реальное положение которого отличается от того что передано в конструктор на веткор `0:-0,5`. Имеет размер `0,8:1,5`. И скорость `0:0`.
+
+Имеет свойство `type` равное `player`.
+
+[bitbucket]: https://bitbucket.org/
+[github]: https://github.com/
+[codepen]: https://codepen.io/
+[Repl.it]: https://repl.it/
diff --git a/css/main.css b/css/main.css
new file mode 100644
index 0000000..982443f
--- /dev/null
+++ b/css/main.css
@@ -0,0 +1,56 @@
+hidden {
+ display: none;
+}
+
+.background {
+ background: rgb(52, 166, 251);
+ table-layout: fixed;
+ border-spacing: 0;
+}
+
+.background td {
+ padding: 0;
+}
+
+.fireball {
+ background: rgb(255, 100, 100);
+}
+
+.lava {
+ background: rgb(255, 100, 100);
+}
+
+.elevator {
+ background: rgb(229, 229, 229);
+}
+
+.wall {
+ background: white;
+}
+
+.actor {
+ position: absolute;
+}
+
+.coin {
+ background: rgb(241, 229, 89);
+}
+
+.player {
+ background: rgb(64, 64, 64);
+}
+
+.lost .player {
+ background: rgb(160, 64, 64);
+}
+
+.won .player {
+ box-shadow: -4px -7px 8px white, 4px -7px 8px white;
+}
+
+.game {
+ overflow: hidden;
+ max-width: 600px;
+ max-height: 450px;
+ position: relative;
+}
diff --git a/game.js b/game.js
new file mode 100644
index 0000000..ad9a93a
--- /dev/null
+++ b/game.js
@@ -0,0 +1 @@
+'use strict';
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..819409d
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..d8241ec
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,296 @@
+'use strict';
+
+function loadLevels() {
+ return new Promise((done, fail) => {
+ const xhr = new XMLHttpRequest();
+ let url = './levels.json';
+ if (location.hostname !== 'localhost') {
+ url = 'https://netology-fbb-store-api.herokuapp.com/game-levels/';
+ }
+ xhr.open('GET', url);
+ xhr.addEventListener('error', e => fail(xhr));
+ xhr.addEventListener('load', e => {
+ if (xhr.status !== 200) {
+ fail(xhr);
+ }
+ done(xhr.responseText);
+ });
+ xhr.send();
+ });
+}
+
+const scale = 30;
+const maxStep = 0.05;
+const wobbleSpeed = 8, wobbleDist = 0.07;
+const playerXSpeed = 7;
+const gravity = 30;
+const jumpSpeed = 17;
+
+function elt(name, className) {
+ var elt = document.createElement(name);
+ if (className) elt.className = className;
+ return elt;
+}
+
+class DOMDisplay {
+ constructor(parent, level) {
+ this.wrap = parent.appendChild(elt("div", "game"));
+ this.wrap.setAttribute('autofocus', true)
+ this.level = level;
+
+ this.actorMap = new Map();
+ this.wrap.appendChild(this.drawBackground());
+ this.actorLayer = this.wrap.appendChild(this.drawActors());
+ this.drawFrame();
+ }
+
+ drawBackground() {
+ var table = elt("table", "background");
+ table.style.width = this.level.width * scale + "px";
+ this.level.grid.forEach(function(row) {
+ var rowElt = table.appendChild(elt("tr"));
+ rowElt.style.height = scale + "px";
+ row.forEach(function(type) {
+ rowElt.appendChild(elt("td", type));
+ });
+ });
+ return table;
+ }
+
+ drawActor(actor) {
+ return elt('div', `actor ${actor.type}`);
+ }
+
+ updateActor(actor, rect) {
+ rect.style.width = actor.size.x * scale + "px";
+ rect.style.height = actor.size.y * scale + "px";
+ rect.style.left = actor.pos.x * scale + "px";
+ rect.style.top = actor.pos.y * scale + "px";
+ }
+
+ drawActors() {
+ var wrap = elt('div');
+ this.level.actors.forEach(actor => {
+ const rect = wrap.appendChild(this.drawActor(actor));
+ this.actorMap.set(actor, rect);
+ });
+ return wrap;
+ }
+
+ updateActors() {
+ for (const [actor, rect] of this.actorMap) {
+ if (this.level.actors.includes(actor)) {
+ this.updateActor(actor, rect);
+ } else {
+ this.actorMap.delete(actor);
+ rect.parentElement.removeChild(rect);
+ }
+ }
+ }
+
+ drawFrame() {
+ this.updateActors();
+
+ this.wrap.className = "game " + (this.level.status || "");
+ this.scrollPlayerIntoView();
+ }
+
+ scrollPlayerIntoView() {
+ var width = this.wrap.clientWidth;
+ var height = this.wrap.clientHeight;
+ var margin = width / 3;
+
+ // The viewport
+ var left = this.wrap.scrollLeft, right = left + width;
+ var top = this.wrap.scrollTop, bottom = top + height;
+
+ var player = this.level.player;
+ if (!player) {
+ return;
+ }
+ var center = player.pos.plus(player.size.times(0.5))
+ .times(scale);
+
+ if (center.x < left + margin)
+ this.wrap.scrollLeft = center.x - margin;
+ else if (center.x > right - margin)
+ this.wrap.scrollLeft = center.x + margin - width;
+ if (center.y < top + margin)
+ this.wrap.scrollTop = center.y - margin;
+ else if (center.y > bottom - margin)
+ this.wrap.scrollTop = center.y + margin - height;
+ }
+
+ clear() {
+ this.wrap.parentNode.removeChild(this.wrap);
+ }
+}
+
+var arrowCodes = {37: "left", 38: "up", 39: "right"};
+
+function trackKeys(codes) {
+ var pressed = Object.create(null);
+ function handler(event) {
+ if (codes.hasOwnProperty(event.keyCode)) {
+ var down = event.type == "keydown";
+ pressed[codes[event.keyCode]] = down;
+ event.preventDefault();
+ }
+ }
+ addEventListener("keydown", handler);
+ addEventListener("keyup", handler);
+ return pressed;
+}
+
+function runAnimation(frameFunc) {
+ var lastTime = null;
+ function frame(time) {
+ var stop = false;
+ if (lastTime != null) {
+ var timeStep = Math.min(time - lastTime, 100) / 1000;
+ stop = frameFunc(timeStep) === false;
+ }
+ lastTime = time;
+ if (!stop) {
+ requestAnimationFrame(frame);
+ }
+ }
+ requestAnimationFrame(frame);
+}
+
+function runLevel(level, Display) {
+ initGameObjects();
+ return new Promise(done => {
+ var arrows = trackKeys(arrowCodes);
+ var display = new Display(document.body, level);
+ runAnimation(step => {
+ level.act(step, arrows);
+ display.drawFrame(step);
+ if (level.isFinished()) {
+ display.clear();
+ done(level.status);
+ return false;
+ }
+ });
+ });
+}
+
+function initGameObjects() {
+ if (initGameObjects.isInit) {
+ return;
+ }
+
+ initGameObjects.isInit = true;
+
+ Level.prototype.act = function(step, keys) {
+ if (this.status !== null) {
+ this.finishDelay -= step;
+ }
+
+ while (step > 0) {
+ var thisStep = Math.min(step, maxStep);
+ this.actors.forEach(actor => {
+ actor.act(thisStep, this, keys);
+ });
+
+ if (this.status === 'lost') {
+ this.player.pos.y += thisStep;
+ this.player.size.y -= thisStep;
+ }
+
+ step -= thisStep;
+ }
+ };
+
+ Player.prototype.handleObstacle = function (obstacle) {
+ if (this.wontJump) {
+ this.speed.y = -jumpSpeed;
+ } else {
+ this.speed.y = 0;
+ }
+ };
+
+ Player.prototype.move = function (motion, level) {
+ var newPos = this.pos.plus(motion);
+ var obstacle = level.obstacleAt(newPos, this.size);
+ if (obstacle) {
+ level.playerTouched(obstacle);
+ this.handleObstacle(obstacle);
+ } else {
+ this.pos = newPos;
+ }
+ };
+
+ Player.prototype.moveX = function (step, level, keys) {
+ this.speed.x = 0;
+ if (keys.left) this.speed.x -= playerXSpeed;
+ if (keys.right) this.speed.x += playerXSpeed;
+
+ var motion = new Vector(this.speed.x, 0).times(step);
+ this.move(motion, level);
+ return;
+ var newPos = this.pos.plus(motion);
+ var obstacle = level.obstacleAt(newPos, this.size);
+ if (obstacle) {
+ level.playerTouched(obstacle);
+
+ }
+ else
+ this.pos = newPos;
+ };
+
+ Player.prototype.moveY = function (step, level, keys) {
+ this.speed.y += step * gravity;
+ this.wontJump = keys.up && this.speed.y > 0;
+
+ var motion = new Vector(0, this.speed.y).times(step);
+ this.move(motion, level);
+ return;
+ var newPos = this.pos.plus(motion);
+ var obstacle = level.obstacleAt(newPos, this.size);
+ if (obstacle) {
+ level.playerTouched(obstacle);
+
+ if (keys.up && this.speed.y > 0) {
+ this.speed.y = -jumpSpeed;
+ } else {
+ this.speed.y = 0;
+ }
+
+ } else {
+ this.pos = newPos;
+ }
+ };
+
+ Player.prototype.act = function (step, level, keys) {
+ this.moveX(step, level, keys);
+ this.moveY(step, level, keys);
+
+ var otherActor = level.actorAt(this);
+ if (otherActor) {
+ level.playerTouched(otherActor.type, otherActor);
+ }
+ };
+}
+
+function runGame(plans, Parser, Display) {
+ return new Promise(done => {
+ function startLevel(n) {
+ runLevel(Parser.parse(plans[n]), Display)
+ .then(status => {
+ if (status == "lost") {
+ startLevel(n);
+ } else if (n < plans.length - 1) {
+ startLevel(n + 1);
+ } else {
+ done();
+ }
+ });
+ }
+ startLevel(0);
+ });
+}
+
+function rand(max = 10, min = 0) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
diff --git a/levels.json b/levels.json
new file mode 100644
index 0000000..ab9d3a1
--- /dev/null
+++ b/levels.json
@@ -0,0 +1,96 @@
+[
+ [
+ " v ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " |xxx w ",
+ " o o ",
+ " x = x ",
+ " x o o x ",
+ " x @ * xxxxx x ",
+ " xxxxx x ",
+ " x!!!!!!!!!!!!!x ",
+ " xxxxxxxxxxxxxxx ",
+ " "
+ ],
+ [
+ " v ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " | ",
+ " o o ",
+ " x = x ",
+ " x o o x ",
+ " x @ xxxxx x ",
+ " xxxxx x ",
+ " x!!!!!!!!!!!!!x ",
+ " xxxxxxxxxxxxxxx ",
+ " "
+ ],
+ [
+ " | | ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " | ",
+ " ",
+ " = | ",
+ " @ | o o ",
+ "xxxxxxxxx!!!!!!!xxxxxxx",
+ " "
+ ],
+ [
+ " ",
+ " ",
+ " ",
+ " o ",
+ " x | x!!x= ",
+ " x ",
+ " x",
+ " ",
+ " ",
+ " ",
+ " xxx ",
+ " ",
+ " ",
+ " xxx | ",
+ " ",
+ " @ ",
+ "xxx ",
+ " "
+ ], [
+ " v v",
+ " ",
+ " !o! ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " xxx ",
+ " o ",
+ " = ",
+ " @ ",
+ " xxxx ",
+ " | ",
+ " xxx x",
+ " ",
+ " ! ",
+ " ",
+ " ",
+ " o x ",
+ " x x ",
+ " x ",
+ " x ",
+ " xx ",
+ " "
+ ]
+]
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b3e1818
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "js-game",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.html",
+ "scripts": {
+ "start": "browser-sync start --server --files \"**/*.*\"",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "browser-sync": "^2.18.8"
+ }
+}
diff --git a/test/actor.spec.js b/test/actor.spec.js
new file mode 100644
index 0000000..f442bfc
--- /dev/null
+++ b/test/actor.spec.js
@@ -0,0 +1,225 @@
+'use strict';
+
+describe('Класс Actor', () => {
+ const position = new Vector(30, 50);
+ const size = new Vector(5, 5);
+
+ describe('Конструктор new Actor()', () => {
+ it('Создает объект со свойством pos, который является вектором', () => {
+ const player = new Actor();
+
+ expect(player.pos).is.instanceof(Vector);
+ });
+
+ it('Создает объект со свойством size, который является вектором', () => {
+ const player = new Actor();
+
+ expect(player.size).is.instanceof(Vector);
+ });
+
+ it('Создает объект со свойством speed, который является вектором', () => {
+ const player = new Actor();
+
+ expect(player.speed).is.instanceof(Vector);
+ });
+
+ it('Создает объект со свойством type, который является строкой', () => {
+ const player = new Actor();
+
+ expect(player.type).to.be.a('string');
+ });
+
+ it('Создает объект с методом act', () => {
+ const player = new Actor();
+
+ expect(player.act).is.instanceof(Function);
+ });
+
+ it('По умолчанию создается объект расположенный в точке 0:0', () => {
+ const player = new Actor();
+
+ expect(player.pos).is.eql(new Vector(0, 0));
+ });
+
+ it('По умолчанию создается объект расмером 1x1', () => {
+ const player = new Actor();
+
+ expect(player.size).is.eql(new Vector(1, 1));
+ });
+
+ it('По умолчанию создается объект со скоростью 0:0', () => {
+ const player = new Actor();
+
+ expect(player.speed).is.eql(new Vector(0, 0));
+ });
+
+ it('По умолчанию создается объект со свойством type равным actor', () => {
+ const player = new Actor();
+
+ expect(player.type).to.equal('actor');
+ });
+
+ it('Свойство type нельзя изменить', () => {
+ const player = new Actor();
+
+ function fn() {
+ player.type = 'player';
+ }
+
+ expect(fn).to.throw(Error);
+ });
+
+ it('Создает объект в заданном расположении, если передать вектор первым аргументом', () => {
+ const player = new Actor(position);
+
+ expect(player.pos).is.equal(position);
+ });
+
+ it('Бросает исключение, если передать не вектор в качестве расположения', () => {
+
+ function fn() {
+ const player = new Actor({ x: 12, y: 24 });
+ }
+
+ expect(fn).to.throw(Error);
+ });
+
+ it('Создает объект заданного размера, если передать вектор вторым аргументом', () => {
+ const player = new Actor(undefined, size);
+
+ expect(player.size).is.equal(size);
+ });
+
+ it('Бросает исключение, если передать не вектор в качестве размера', () => {
+
+ function fn() {
+ const player = new Actor(undefined, { x: 12, y: 24 });
+ }
+
+ expect(fn).to.throw(Error);
+ });
+
+ it('Бросает исключение, если передать не вектор в качестве скорости', () => {
+
+ function fn() {
+ const player = new Actor(undefined, undefined, { x: 12, y: 24 });
+ }
+
+ expect(fn).to.throw(Error);
+ });
+ });
+
+ describe('Границы объекта', () => {
+ it('Имеет свойство left, которое содержит координату левой границы объекта по оси X', () => {
+ const player = new Actor(position, size);
+
+ expect(player.left).is.equal(position.x);
+ });
+
+ it('Имеет свойство right, которое содержит координату правой границы объекта оп оси X', () => {
+ const player = new Actor(position, size);
+
+ expect(player.right).is.equal(position.x + size.x);
+ });
+
+ it('Имеет свойство top, которое содержит координату верхней границы объекта по оси Y', () => {
+ const player = new Actor(position, size);
+
+ expect(player.top).is.equal(position.y);
+ });
+
+ it('Имеет свойство bottom, которое содержит координату правой границы объекта оп оси Y', () => {
+ const player = new Actor(position, size);
+
+ expect(player.bottom).is.equal(position.y + size.y);
+ });
+ });
+
+ describe('Метод isIntersect', () => {
+ it('Если передать объект не являющийся экземпляром Actor, то получим исключение', () => {
+ const player = new Actor();
+
+ function fn() {
+ player.isIntersect({ left: 0, top: 0, bottom: 1, right: 1 });
+ }
+
+ expect(fn).to.throw(Error);
+ });
+
+ it('Объект не пересекается сам с собой', () => {
+ const player = new Actor(position, size);
+
+ const notIntersected = player.isIntersect(player);
+
+ expect(notIntersected).is.equal(false);
+ });
+
+ it('Объект не пересекается с объектом расположенным очень далеко', () => {
+ const player = new Actor(new Vector(0, 0));
+ const coin = new Actor(new Vector(100, 100));
+
+ const notIntersected = player.isIntersect(coin);
+
+ expect(notIntersected).is.equal(false);
+ });
+
+ it('Объект не пересекается с объектом со смежными границами', () => {
+ const player = new Actor(position, size);
+
+ const moveX = new Vector(1, 0);
+ const moveY = new Vector(0, 1);
+
+ const coins = [
+ new Actor(position.plus(moveX.times(-1))),
+ new Actor(position.plus(moveY.times(-1))),
+ new Actor(position.plus(size).plus(moveX)),
+ new Actor(position.plus(size).plus(moveY))
+ ];
+
+ coins.forEach(coin => {
+ const notIntersected = player.isIntersect(coin);
+
+ expect(notIntersected).is.equal(false);
+ });
+ });
+
+ it('Объект не пересекается с объектом расположенным в той же точке, но имеющим отрицательный вектор размера', () => {
+ const player = new Actor(new Vector(0, 0), new Vector(1, 1));
+ const coin = new Actor(new Vector(0, 0), new Vector(1, 1).times(-1));
+
+ const notIntersected = player.isIntersect(coin);
+
+ expect(notIntersected).is.equal(false);
+ });
+
+ it('Объект пересекается с объектом, который полностью содержится в нём', () => {
+ const player = new Actor(new Vector(0, 0), new Vector(100, 100));
+ const coin = new Actor(new Vector(10, 10), new Vector());
+
+ const intersected = player.isIntersect(coin);
+
+ expect(intersected).is.equal(true);
+ });
+
+ it('Объект пересекается с объектом, который частично содержится в нём', () => {
+ const player = new Actor(position, size);
+
+ const moveX = new Vector(1, 0);
+ const moveY = new Vector(0, 1);
+
+ const coins = [
+ new Actor(position.plus(moveX.times(-1)), size),
+ new Actor(position.plus(moveY.times(-1)), size),
+ new Actor(position.plus(moveX), size),
+ new Actor(position.plus(moveY), size)
+ ];
+
+ coins.forEach(coin => {
+ const intersected = player.isIntersect(coin);
+
+ expect(intersected).is.equal(true);
+ });
+ });
+
+ });
+});
diff --git a/test/coin.spec.js b/test/coin.spec.js
new file mode 100644
index 0000000..39d1355
--- /dev/null
+++ b/test/coin.spec.js
@@ -0,0 +1,152 @@
+'use strict';
+
+describe('Класс Coin', () => {
+ const position = new Vector(5, 5);
+
+ describe('Конструктор new Coin', () => {
+ it('Создает экземпляр Actor', () => {
+ const coin = new Coin();
+
+ expect(coin).to.be.an.instanceof(Actor);
+ });
+
+ it('Имеет свойство type равное coin', () => {
+ const coin = new Coin();
+
+ expect(coin.type).to.equal('coin');
+ });
+
+ it('Имеет размер Vector(0.6, 0.6)', () => {
+ const coin = new Coin();
+
+ expect(coin.size).to.eql(new Vector(0.6, 0.6));
+ });
+
+ it('Реальная позициия сдвинута на Vector(0.2, 0.1)', () => {
+ const coin = new Coin(position);
+ const realPosition = position.plus(new Vector(0.2, 0.1));
+
+ expect(coin.pos).to.eql(realPosition);
+ });
+
+ it('Имеет свойство spring равное случайному числу от 0 до 2π', () => {
+ const coin = new Coin();
+
+ expect(coin.spring).to.be.within(0, 2 * Math.PI);
+ });
+
+ it('Имеет свойство springSpeed равное 8', () => {
+ const coin = new Coin();
+
+ expect(coin.springSpeed).to.equal(8);
+ });
+
+ it('Имеет свойство springDist равное 0.07', () => {
+ const coin = new Coin();
+
+ expect(coin.springDist).to.equal(0.07);
+ });
+ });
+
+ describe('Метод updateSpring', () => {
+ it('Увеличит свойство spring на springSpeed', () => {
+ const coin = new Coin();
+ const initialSpring = coin.spring;
+
+ coin.updateSpring();
+
+ expect(coin.spring).to.equal(initialSpring + coin.springSpeed);
+ });
+
+ it('Если передать время, увеличит свойство spring на springSpeed умноженное на время', () => {
+ const time = 5;
+ const coin = new Coin();
+ const initialSpring = coin.spring;
+
+ coin.updateSpring(time);
+
+ expect(coin.spring).to.equal(initialSpring + (coin.springSpeed * time));
+ });
+ });
+
+ describe('Метод getSpringVector', () => {
+ it('Вернет вектор', () => {
+ const coin = new Coin();
+
+ const vector = coin.getSpringVector();
+
+ expect(vector).to.be.an.instanceof(Vector);
+ });
+
+ it('Координата x этого вектора равна нулю', () => {
+ const coin = new Coin();
+
+ const vector = coin.getSpringVector();
+
+ expect(vector.x).to.equal(0);
+ });
+
+ it('Координата y этого вектора равна синусу от spring, умноженному на springDist', () => {
+ const coin = new Coin();
+
+ const vector = coin.getSpringVector();
+
+ expect(vector.y).to.equal(Math.sin(coin.spring) * coin.springDist);
+ });
+ });
+
+ describe('Метод getNextPosition', () => {
+ it('Увеличит sping на springSpeed', () => {
+ const coin = new Coin(position);
+ const initialSpring = coin.spring;
+
+ coin.getNextPosition();
+
+ expect(coin.spring).to.equal(initialSpring + coin.springSpeed);
+ });
+
+ it('Если передать время, увеличит свойство spring на springSpeed умноженное на время', () => {
+ const time = 5;
+ const coin = new Coin();
+ const initialSpring = coin.spring;
+
+ coin.getNextPosition(time);
+
+ expect(coin.spring).to.equal(initialSpring + (coin.springSpeed * time));
+ });
+
+ it('Вернет вектор', () => {
+ const coin = new Coin(position);
+
+ const newPosition = coin.getNextPosition();
+
+ expect(newPosition).to.be.an.instanceof(Vector);
+ });
+
+ it('Координата x новой позиции не изменится', () => {
+ const coin = new Coin(position);
+ const realPosition = coin.pos;
+
+ const newPosition = coin.getNextPosition();
+
+ expect(newPosition.x).to.equal(realPosition.x);
+ });
+
+ it('Координата y новой позиции будет в пределах исходного значения y и y + 1', () => {
+ const coin = new Coin(position);
+
+ const newPosition = coin.getNextPosition();
+ expect(newPosition.y).to.be.within(position.y, position.y + 1);
+ });
+
+ it('Вернет новую позицию увеличив старую на вектор подпрыгивания', () => {
+ const coin = new Coin(position);
+ const realPosition = coin.pos;
+
+ const newPosition = coin.getNextPosition();
+ const springVector = coin.getSpringVector();
+
+ expect(newPosition).to.eql(realPosition.plus(springVector));
+ });
+ });
+});
diff --git a/test/fireball.spec.js b/test/fireball.spec.js
new file mode 100644
index 0000000..3aa069c
--- /dev/null
+++ b/test/fireball.spec.js
@@ -0,0 +1,70 @@
+'use strict';
+
+describe('Класс Fireball', () => {
+ const time = 5;
+ const speed = new Vector(1, 0);
+ const position = new Vector(5, 5);
+
+ describe('Конструктор new Fireball', () => {
+ it('Созданный объект является экземпляром Actor', () => {
+ const ball = new Fireball();
+
+ expect(ball).to.be.an.instanceof(Actor);
+ });
+
+ it('Имеет свойство type равное fireball', () => {
+ const ball = new Fireball();
+
+ expect(ball.type).to.equal('fireball');
+ });
+
+ it('Имеет свойство speed равное вектору Vector переданному вторым аргументом', () => {
+ const ball = new Fireball(undefined, speed);
+
+ expect(ball.speed).to.eql(speed);
+ });
+
+ it('Свойство pos равно вектору Vector переданному первым аргументом', () => {
+ const ball = new Fireball(position);
+
+ expect(ball.pos).to.equal(position);
+ });
+ });
+
+ describe('Метод getNextPosition', () => {
+ it('Вернет ту же позицию для объекта с нулевой скоростью', () => {
+ const zeroSpeed = new Vector(0, 0);
+ const ball = new Fireball(position, zeroSpeed);
+
+ const nextPosition = ball.getNextPosition();
+
+ expect(nextPosition).to.eql(position);
+ });
+
+ it('Вернет новую позицию, увеличенную на вектор скорости', () => {
+ const ball = new Fireball(position, speed);
+
+ const nextPosition = ball.getNextPosition();
+
+ expect(nextPosition).to.eql(position.plus(speed));
+ });
+
+ it('Если передать время первым аргументом, то вернет новую позицию увелеченную на вектор скорости помноженный на переданное время', () => {
+ const ball = new Fireball(position, speed);
+
+ const nextPosition = ball.getNextPosition(time);
+
+ expect(nextPosition).to.eql(position.plus(speed.times(time)));
+ });
+ });
+
+ describe('Метод handleObstacle', () => {
+ it('Меняет вектор скорости на противоположный', () => {
+ const ball = new Fireball(position, speed);
+
+ ball.handleObstacle();
+
+ expect(ball.speed).to.eql(speed.times(-1));
+ });
+ });
+});
diff --git a/test/firerain.spec.js b/test/firerain.spec.js
new file mode 100644
index 0000000..dd85011
--- /dev/null
+++ b/test/firerain.spec.js
@@ -0,0 +1,44 @@
+'use strict';
+
+describe('Класс FireRain', () => {
+ describe('Конструктор new FireRain', () => {
+ it('Создает экземпляр Fireball', () => {
+ const ball = new FireRain();
+
+ expect(ball).to.be.an.instanceof(Fireball);
+ });
+
+ it('Имеет скорость Vector(0, 3)', () => {
+ const ball = new FireRain();
+
+ expect(ball.speed).to.eql(new Vector(0, 3));
+ });
+
+ it('Имеет свойство type равное fireball', () => {
+ const ball = new HorizontalFireball();
+
+ expect(ball.type).to.equal('fireball');
+ });
+ });
+
+ describe('Метод handleObstacle', () => {
+ const position = new Vector(5, 5);
+
+ it('Не меняет вектор скорости', () => {
+ const ball = new FireRain(position);
+
+ ball.handleObstacle();
+
+ expect(ball.speed).to.eql(new Vector(0, 3));
+ });
+
+ it('Меняет позицию на исходную', () => {
+ const ball = new FireRain(position);
+ ball.pos = new Vector(100, 100);
+
+ ball.handleObstacle();
+
+ expect(ball.pos).to.eql(position);
+ });
+ });
+});
diff --git a/test/horizontalfireball.spec.js b/test/horizontalfireball.spec.js
new file mode 100644
index 0000000..5bcfed8
--- /dev/null
+++ b/test/horizontalfireball.spec.js
@@ -0,0 +1,23 @@
+'use strict';
+
+describe('Класс HorizontalFireball', () => {
+ describe('Конструктор new HorizontalFireball', () => {
+ it('Создает экземпляр Fireball', () => {
+ const ball = new HorizontalFireball();
+
+ expect(ball).to.be.an.instanceof(Fireball);
+ });
+
+ it('Имеет скорость Vector(2, 0)', () => {
+ const ball = new HorizontalFireball();
+
+ expect(ball.speed).to.eql(new Vector(2, 0));
+ });
+
+ it('Имеет свойство type равное fireball', () => {
+ const ball = new HorizontalFireball();
+
+ expect(ball.type).to.equal('fireball');
+ });
+ });
+});
diff --git a/test/index.html b/test/index.html
new file mode 100644
index 0000000..f3d7956
--- /dev/null
+++ b/test/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/level.spec.js b/test/level.spec.js
new file mode 100644
index 0000000..4e27172
--- /dev/null
+++ b/test/level.spec.js
@@ -0,0 +1,293 @@
+'use strict';
+
+describe('Класс Level', () => {
+ const player = new Actor();
+
+ describe('Конструктор new Level', () => {
+ it('Высота пустого уровня равна 0', () => {
+ const level = new Level();
+
+ expect(level.height).to.equal(0);
+ });
+
+ it('Ширина пустого уровня равна 0', () => {
+ const level = new Level();
+
+ expect(level.width).to.equal(0);
+ });
+
+ it('Высота уровня равна количеству строк сетки', () => {
+ const lines = 100;
+ const grid = new Array(lines);
+
+ const level = new Level(grid);
+
+ expect(level.height).to.equal(lines);
+ });
+
+ it('Ширина уровня равна количеству ячеек сетки', () => {
+ const lines = 100;
+ const cells = 50;
+ const grid = new Array(lines).fill(new Array(cells));
+
+ const level = new Level(grid);
+
+ expect(level.width).to.equal(cells);
+ });
+
+ it('Если в строках разное количество ячеек, то ширина уровня равна количеству ячеек в самой длинной строке', () => {
+ const lines = 100;
+ const cells = 50;
+ const maxCells = 100;
+ const grid = new Array(lines).fill(new Array(cells));
+ grid[73].length = maxCells;
+
+ const level = new Level(grid);
+
+ expect(level.width).to.equal(maxCells);
+ });
+
+ it('Имеет свойство status равное null', () => {
+ const level = new Level();
+
+ expect(level.status).to.be.null;
+ });
+
+ it('Имеет свойство finishDelay равное 1', () => {
+ const level = new Level();
+
+ expect(level.finishDelay).to.equal(1);
+ });
+ });
+
+ describe('Метод isFinished', () => {
+ it('По умолчанию вернет false', () => {
+ const level = new Level();
+
+ const isNotFinished = level.isFinished();
+
+ expect(isNotFinished).to.be.false;
+ });
+
+ it('Вернут true, если status будет не равен null, и finishDelay меньше нуля', () => {
+ const level = new Level();
+
+ level.status = 'lost';
+ level.finishDelay = -1;
+ const isFinished = level.isFinished();
+
+ expect(isFinished).to.be.true;
+ });
+
+ it('Вернут false, если status будет не равен null, но finishDelay будет больше нуля', () => {
+ const level = new Level();
+
+ level.status = 'lost';
+ const isNotFinished = level.isFinished();
+
+ expect(isNotFinished).to.be.false;
+ });
+ });
+
+ describe('Метод actorAt', () => {
+ const coin = new Actor();
+
+ it('Выбросит исключение если передать не движущийся объект Actor', () => {
+ const level = new Level(undefined, [player]);
+
+ function fn() {
+ level.actorAt({});
+ }
+
+ expect(fn).to.throw(Error);
+ });
+
+ it('Вернет undefined для пустого уровня', () => {
+ const level = new Level();
+
+ const noActor = level.actorAt(player);
+
+ expect(noActor).to.be.undefined;
+ });
+
+ it('Вернет undefined для уровня в котором только один движущийся объект', () => {
+ const level = new Level(undefined, [player]);
+
+ const noActor = level.actorAt(player);
+
+ expect(noActor).to.be.undefined;
+ });
+
+ it('Вернет undefined если ни один объект игрового поля не пересекается с переданным объектом', () => {
+ const level = new Level(undefined, [player, coin]);
+ player.move(1, 1);
+
+ const actor = level.actorAt(player);
+
+ expect(actor).to.be.equal(coin);
+ });
+
+ it('Вернет объект игрового поля, который пересекается с переданным объектом', () => {
+ const level = new Level(undefined, [player, coin]);
+
+ const actor = level.actorAt(player);
+
+ expect(actor).to.be.equal(coin);
+ });
+
+ });
+
+ describe('Метод obstacleAt', () => {
+ const gridSize = 2;
+ const grid = new Array(gridSize).fill(new Array(gridSize));
+ const wallGrid = new Array(gridSize).fill(new Array(gridSize).fill('wall'));
+ const lavaGrid = new Array(gridSize).fill(new Array(gridSize).fill('lava'));
+ const size = new Vector(1, 1);
+
+ it('Вернет undefined если объект не выходит за пределы уровня и ни с чем не пересекается', () => {
+ const level = new Level(grid);
+ const position = new Vector(0, 0);
+
+ const wall = level.obstacleAt(position, size);
+
+ expect(wall).to.be.undefined;
+ });
+
+ it('Вернет строку wall если левая граница объекта выходит за пределы уровня', () => {
+ const level = new Level(grid);
+ const position = new Vector(-1, 0);
+
+ const wall = level.obstacleAt(position, size);
+
+ expect(wall).to.be.equal('wall');
+ });
+
+ it('Вернет строку wall если правая граница объекта выходит за пределы уровня', () => {
+ const level = new Level(grid);
+ const position = new Vector(gridSize, 0);
+
+ const wall = level.obstacleAt(position, size);
+
+ expect(wall).to.be.equal('wall');
+ });
+
+ it('Вернет строку wall если верхняя граница объекта выходит за пределы уровня', () => {
+ const level = new Level(grid);
+ const position = new Vector(0, -1);
+
+ const wall = level.obstacleAt(position, size);
+
+ expect(wall).to.be.equal('wall');
+ });
+
+ it('Вернет строку lava если нижняя граница объекта выходит за пределы уровня', () => {
+ const level = new Level(grid);
+ const position = new Vector(0, gridSize);
+
+ const wall = level.obstacleAt(position, size);
+
+ expect(wall).to.be.equal('lava');
+ });
+
+ it('Вернет строку wall если площадь пересекается со стеной', () => {
+ const level = new Level(wallGrid);
+ const position = new Vector(0, 0);
+
+ const wall = level.obstacleAt(position, size);
+
+ expect(wall).to.be.equal('wall');
+ });
+
+ it('Вернет строку lava если площадь пересекается с лавой', () => {
+ const level = new Level(lavaGrid);
+ const position = new Vector(0, 0);
+
+ const wall = level.obstacleAt(position, size);
+
+ expect(wall).to.be.equal('lava');
+ });
+ });
+
+ describe('Метод removeActor', () => {
+ const coin = new Actor();
+ const lava = new Actor();
+
+ it('Удаляет переданный движущийся объект', () => {
+ const level = new Level(undefined, [ coin, lava ]);
+
+ level.removeActor(coin);
+
+ expect(level.actors.includes(coin)).to.be.false;
+ });
+
+ it('Не удаляет остальные движущиеся объекты', () => {
+ const level = new Level(undefined, [ coin, lava ]);
+
+ level.removeActor(coin);
+
+ expect(level.actors.includes(lava)).to.be.true;
+ });
+ });
+
+ describe('Метод noMoreActors', () => {
+ const coin = { type: 'coin' };
+ const lava = { type: 'lava' };
+
+ it('Вернет истину, если движущихся объектов нет в уровне', () => {
+ const level = new Level();
+
+ expect(level.noMoreActors()).to.be.true;
+ });
+
+ it('Вернет истину, если в уровне нет движущихся объектов заданного типа', () => {
+ const level = new Level(undefined, [ coin, lava ]);
+
+ expect(level.noMoreActors('actor')).to.be.true;
+ });
+
+ it('Вернет ложь, если в уровне есть движущихся объекты заданного типа', () => {
+ const level = new Level(undefined, [ coin, lava ]);
+
+ expect(level.noMoreActors('coin')).to.be.false;
+ });
+ });
+
+ describe('Метод playerTouched', () => {
+ const goldCoin = { type: 'coin', title: 'Золото' };
+ const bronzeCoin = { type: 'coin', title: 'Бронза' };
+
+ it('Если передать lava первым аргументом, меняет статус уровня на lost', () => {
+ const level = new Level();
+
+ level.playerTouched('lava');
+
+ expect(level.status).to.equal('lost');
+ });
+
+ it('Если передать fireball первым аргументом, меняет статус уровня на lost', () => {
+ const level = new Level();
+
+ level.playerTouched('fireball');
+
+ expect(level.status).to.equal('lost');
+ });
+
+ it('Если передать coin первым аргументом и движущийся объект вторым, удаляет этот объект из уровня', () => {
+ const level = new Level(undefined, [ goldCoin, bronzeCoin ]);
+
+ level.playerTouched('coin', goldCoin);
+
+ expect(level.actors).to.have.length(1);
+ expect(level.actors).to.not.include(goldCoin);
+ });
+
+ it('Если удалить все монеты, то статус меняется на won', () => {
+ const level = new Level(undefined, [ goldCoin, bronzeCoin ]);
+
+ level.playerTouched('coin', goldCoin);
+ level.playerTouched('coin', bronzeCoin);
+
+ expect(level.status).to.equal('won');
+ });
+ });
+});
diff --git a/test/parser.spec.js b/test/parser.spec.js
new file mode 100644
index 0000000..cdede54
--- /dev/null
+++ b/test/parser.spec.js
@@ -0,0 +1,242 @@
+'use strict';
+
+describe('Класс LevelParser', () => {
+ class MyActor {}
+
+ describe('Конструктор new LevelParser()', () => {
+ });
+
+ describe('Метод actorFromSymbol', () => {
+
+
+ it('Вернет undefined, если не передать символ', () => {
+ const parser = new LevelParser();
+
+ const actor = parser.actorFromSymbol();
+
+ expect(actor).to.be.undefined;
+ });
+
+ it('Вернет undefined, если передать символ которому не назначен конструктор движимого объекта', () => {
+ const parser = new LevelParser({ y: MyActor });
+
+ const actor = parser.actorFromSymbol('z');
+
+ expect(actor).to.be.undefined;
+ });
+
+ it('Вернет подходящий конструктор движимого объекта, если передать символ которому он назначен', () => {
+ const parser = new LevelParser({ y: MyActor });
+
+ const actor = parser.actorFromSymbol('y');
+
+ expect(actor).to.equal(MyActor);
+ });
+ });
+
+ describe('Метод obstacleFromSymbol', () => {
+ it('Вернет undefined, если не передать символ', () => {
+ const parser = new LevelParser();
+
+ const obstacle = parser.obstacleFromSymbol();
+
+ expect(obstacle).to.be.undefined;
+ });
+
+ it('Вернет undefined, если передать неизветсный символ', () => {
+ const parser = new LevelParser();
+
+ const obstacle = parser.obstacleFromSymbol('Z');
+
+ expect(obstacle).to.be.undefined;
+ });
+
+ it('Вернет wall, если передать символ x', () => {
+ const parser = new LevelParser();
+
+ const obstacle = parser.obstacleFromSymbol('x');
+
+ expect(obstacle).to.equal('wall');
+ });
+
+ it('Вернет lava, если передать символ !', () => {
+ const parser = new LevelParser();
+
+ const obstacle = parser.obstacleFromSymbol('!');
+
+ expect(obstacle).to.equal('lava');
+ });
+ });
+
+ describe('Метод createGrid', () => {
+ const plan = [
+ 'x x',
+ '!!!'
+ ];
+
+ it('Вернет пустой массив, если передать пустой план', () => {
+ const parser = new LevelParser();
+
+ const grid = parser.createGrid([]);
+
+ expect(grid).to.eql([]);
+ });
+
+ it('Вернет массив того же размера что и plan', () => {
+ const parser = new LevelParser();
+
+ const grid = parser.createGrid(plan);
+
+ expect(grid.length).to.equal(plan.length);
+ });
+
+ it('В ряду будет столько элементов, сколько символов в строке плана', () => {
+ const parser = new LevelParser();
+
+ const grid = parser.createGrid(plan);
+
+ grid.forEach((row, y) => {
+ expect(row.length).to.equal(plan[y].length);
+ });
+ });
+
+ it('Символы x определит как wall и поместит в соответствующую ячейку', () => {
+ const parser = new LevelParser();
+
+ const grid = parser.createGrid(plan);
+
+ grid.forEach((row, y) => {
+ row.forEach((cell, x) => {
+ if (plan[y][x] === 'x') {
+ expect(cell).to.equal('wall');
+ }
+ })
+ });
+ });
+
+ it('Символы ! определит как lava и поместит в соответствующую ячейку', () => {
+ const parser = new LevelParser();
+
+ const grid = parser.createGrid(plan);
+
+ grid.forEach((row, y) => {
+ row.forEach((cell, x) => {
+ if (plan[y][x] === '!') {
+ expect(cell).to.equal('lava');
+ }
+ })
+ });
+ });
+ });
+
+ describe('Метод createActors', () => {
+ const plan = [
+ 'o o',
+ ' z ',
+ 'o o'
+ ];
+ class MyActor {}
+
+ it('Вернет пустой массив, если не определить символы движущихся объектов', () => {
+ const parser = new LevelParser();
+
+ const actors = parser.createActors(plan);
+
+ expect(actors).to.eql([]);
+ });
+
+ it('Вернет пустой массив, если передать пустой план', () => {
+ const parser = new LevelParser({ o: Actor, z: MyActor });
+
+ const actors = parser.createActors([]);
+
+ expect(actors).to.eql([]);
+ });
+
+ it('Вернет массив со всеми движущимися объектами, если передать план', () => {
+ const parser = new LevelParser({ o: Actor, z: MyActor });
+
+ const actors = parser.createActors(plan);
+
+ expect(actors).to.have.length(5);
+ });
+
+ it('Каждый движущийся объект будет экземпляром своего класса', () => {
+ const parser = new LevelParser({ o: Actor, z: MyActor });
+
+ const actors = parser.createActors(plan);
+ const oActors = actors.filter(actor => actor instanceof Actor);
+ const zActors = actors.filter(actor => actor instanceof MyActor);
+
+ expect(oActors).to.have.length(4);
+ expect(zActors).to.have.length(1);
+ });
+
+ it('Каждый движущийся объект будет иметь координаты той ячейки, где он размещен на плане', () => {
+ const parser = new LevelParser({ o: Actor, z: Actor });
+
+ const actors = parser.createActors(plan);
+
+ expect(actors.some(actor => actor.pos.x === 0 && actor.pos.y === 0)).to.be.true;
+ expect(actors.some(actor => actor.pos.x === 2 && actor.pos.y === 0)).to.be.true;
+ expect(actors.some(actor => actor.pos.x === 1 && actor.pos.y === 1)).to.be.true;
+ expect(actors.some(actor => actor.pos.x === 0 && actor.pos.y === 0)).to.be.true;
+ expect(actors.some(actor => actor.pos.x === 2 && actor.pos.y === 2)).to.be.true;
+ });
+ });
+
+ describe('Метод parse', () => {
+ const plan = [
+ 'oxo',
+ 'xzx',
+ 'oxo'
+ ];
+ class MyActor {}
+
+ it('Вернет объект уровня, Level', () => {
+ const parser = new LevelParser();
+
+ const level = parser.parse([]);
+
+ expect(level).to.be.an.instanceof(Level);
+ });
+
+ it('Высота уровня будет равна количеству строк плана', () => {
+ const parser = new LevelParser();
+
+ const level = parser.parse(plan);
+
+ expect(level.width).to.equal(plan.length);
+ });
+
+ it('Ширина уровня будет равна количеству символов в максимальной строке плана', () => {
+ const parser = new LevelParser();
+
+ const level = parser.parse(plan);
+
+ expect(level.height).to.equal(plan[0].length);
+ });
+
+ it('Создаст уровень с движущимися объектами из плана', () => {
+ const parser = new LevelParser({ x: Actor, z: MyActor });
+
+ const level = parser.parse(plan);
+
+ expect(level.actors).to.have.length(5);
+ });
+
+ it('Создаст уровень с припятствиями из плана', () => {
+ const parser = new LevelParser();
+
+ const level = parser.parse(plan);
+
+ level.grid.forEach((row, y) => {
+ row.forEach((cell, x) => {
+ if (plan[y][x] === 'x') {
+ expect(cell).to.equal('wall');
+ }
+ })
+ });
+ });
+ });
+});
diff --git a/test/vector.spec.js b/test/vector.spec.js
new file mode 100644
index 0000000..8fc2d0d
--- /dev/null
+++ b/test/vector.spec.js
@@ -0,0 +1,71 @@
+'use strict';
+
+describe('Класс Vector', () => {
+ const x = 3, y = 7, left = 5, top = 10, n = 5;
+
+ describe('Конструктор new Vector()', () => {
+ it('создает объект со свойствами x и y равными аргументам конструктора', () => {
+ const position = new Vector(left, top);
+
+ expect(position.x).is.equal(left);
+ expect(position.y).is.equal(top);
+ });
+
+ it('без аргументов создает объект со свойствами x и y равными 0', () => {
+ const position = new Vector();
+
+ expect(position.x).is.equal(0);
+ expect(position.y).is.equal(0);
+ });
+
+ });
+
+ describe('Метод plus()', () => {
+ it('бросает исключение, если передать не вектор', () => {
+ const position = new Vector(x, y);
+
+ function fn() {
+ position.plus({ left, top });
+ }
+
+ expect(fn).to.throw(Error);
+ });
+
+ it('создает новый вектор', () => {
+ const position = new Vector(x, y);
+
+ const newPosition = position.plus(new Vector(left, top));
+
+ expect(newPosition).is.instanceof(Vector);
+ });
+
+ it('координаты нового вектора равны сумме координат суммируемых', () => {
+ const position = new Vector(x, y);
+
+ const newPosition = position.plus(new Vector(left, top));
+
+ expect(newPosition.x).is.equal(x + left);
+ expect(newPosition.y).is.equal(y + top);
+ });
+ });
+
+ describe('Метод times()', () => {
+ it('создает новый вектор', () => {
+ const position = new Vector(x, y);
+
+ const newPosition = position.times(n);
+
+ expect(newPosition).is.instanceof(Vector);
+ });
+
+ it('координаты нового вектора увеличины в n раз', () => {
+ const position = new Vector(x, y);
+
+ const newPosition = position.times(n);
+
+ expect(newPosition.x).is.equal(x * n);
+ expect(newPosition.y).is.equal(y * n);
+ });
+ });
+
+});
diff --git a/test/verticalfireball.spec.js b/test/verticalfireball.spec.js
new file mode 100644
index 0000000..668148c
--- /dev/null
+++ b/test/verticalfireball.spec.js
@@ -0,0 +1,23 @@
+'use strict';
+
+describe('Класс VerticalFireball', () => {
+ describe('Конструктор new VerticalFireball', () => {
+ it('Создает экземпляр Fireball', () => {
+ const ball = new VerticalFireball();
+
+ expect(ball).to.be.an.instanceof(Fireball);
+ });
+
+ it('Имеет скорость Vector(0, 2)', () => {
+ const ball = new VerticalFireball();
+
+ expect(ball.speed).to.eql(new Vector(0, 2));
+ });
+
+ it('Имеет свойство type равное fireball', () => {
+ const ball = new HorizontalFireball();
+
+ expect(ball.type).to.equal('fireball');
+ });
+ });
+});