From 880aab40c4bef20c7ee8743d90fdf7288ae26fbc Mon Sep 17 00:00:00 2001 From: Dima Fitiskin Date: Wed, 10 May 2017 14:26:03 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B0=D1=82=D0=B5=D1=80=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D1=8B=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 774 +++++++++++++++++++++++++++++++- css/main.css | 56 +++ game.js | 1 + index.html | 14 + js/app.js | 296 ++++++++++++ levels.json | 96 ++++ package.json | 16 + test/actor.spec.js | 225 ++++++++++ test/coin.spec.js | 152 +++++++ test/fireball.spec.js | 70 +++ test/firerain.spec.js | 44 ++ test/horizontalfireball.spec.js | 23 + test/index.html | 39 ++ test/level.spec.js | 293 ++++++++++++ test/parser.spec.js | 242 ++++++++++ test/vector.spec.js | 71 +++ test/verticalfireball.spec.js | 23 + 17 files changed, 2433 insertions(+), 2 deletions(-) create mode 100644 css/main.css create mode 100644 game.js create mode 100644 index.html create mode 100644 js/app.js create mode 100644 levels.json create mode 100644 package.json create mode 100644 test/actor.spec.js create mode 100644 test/coin.spec.js create mode 100644 test/fireball.spec.js create mode 100644 test/firerain.spec.js create mode 100644 test/horizontalfireball.spec.js create mode 100644 test/index.html create mode 100644 test/level.spec.js create mode 100644 test/parser.spec.js create mode 100644 test/vector.spec.js create mode 100644 test/verticalfireball.spec.js 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'); + }); + }); +});