├── .gitignore ├── README.md ├── css └── main.css ├── game.js ├── index.html ├── js └── app.js ├── levels.json ├── package.json ├── res └── preview.png └── test ├── actor.spec.js ├── coin.spec.js ├── fireball.spec.js ├── firerain.spec.js ├── horizontalfireball.spec.js ├── index.html ├── level.spec.js ├── parser.spec.js ├── player.spec.js ├── vector.spec.js └── verticalfireball.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Дипломный проект курса JavaScript 2 | === 3 | 4 | В рамках дипломного проекта вам необходимо реализовать ключевые компоненты игры. Игра будет запускаться и работать в браузере. 5 | 6 | Игра после реализации имеет следующий интерфейс: 7 | ![Внешний вид игры](./res/preview.png) 8 | 9 | На иллюстрации вы можете видеть: 10 | - Белые стены препятствий. 11 | - Красные огненные шары и лаву. 12 | - Желтые монетки. 13 | - Игрока бордового цвета, потому что в данный момент он умер от столкновения с огненным шаром. 14 | 15 | Игрок управляется стрелками с клавиатуры. Основная цель каждого уровня — собрать все монетки. 16 | 17 | ## Реализация 18 | 19 | Реализовать проект и предоставить его на проверку можно двумя способами: 20 | + Локально и публиковать код в ваш репозиторий [GitHub] или [BitBucket] 21 | + В онлайн-песочнице [CodePen] или [Repl.it] 22 | 23 | Сама игра будет функционировать, когда вы окончательно реализуете все компоненты. Но чтобы понять, правильно ли реализован каждый из них, для каждого компонента дан пример кода и результат его работы, по которому вы можете проверить, правильно ли вы его реализовали. Сам код примеров в итоговом решении оставлять не рекомендуется. 24 | 25 | Также есть возможность запустить тесты, которые покажут, верно ли реализован каждый компонент. Об этом будет подробно описано в разделе «Тестирование». 26 | 27 | ### Реализация в репозитории 28 | 29 | #### Подготовка репозитория 30 | 31 | 1. Установить git. 32 | 2. Создайте аккаунт в сервисе [GitHub] или [BitBucket]. 33 | 3. Создайте публичный репозиторий. 34 | 4. Скопируйте ссылку на репозиторий (рекомендуем использовать HTTPS, если ранее вы не сталкивались с SSH). 35 | 5. Клонируйте ваш репозиторий локально, используя команду `git clone`. 36 | 37 | Итогом будет наличие папки на локальном компьютере, в которой инициализирован git-репозиторий и настроена связь с репозиторием на [GitHub] или [BitBucket]. 38 | 39 | #### Подготовка проекта 40 | 41 | 1. Скачайте свежую версию проекта по ссылке 42 | 43 | https://github.com/netology-code/js-game/releases 44 | 45 | 2. Разверните архив проекта в папку, созданную при подготовке репозитория. 46 | 3. Ваш код пишите в файле `./game.js`. 47 | 4. Для запуска игры откройте в браузере файл `./index.html`. 48 | 5. Для запуска тестов откройте в браузере файл `./test/index.html`. 49 | 50 | Менять остальные файлы не рекомендуется. 51 | 52 | #### Публикация промежуточных версий 53 | 54 | 1. Добавьте к коммиту файл `game.js` командой `git add game.js`. 55 | 2. Сделайте коммит `git commit`. 56 | 3. Опубликуйте изменения с помощью команды `git push`. 57 | 58 | #### Создание локального сервера (необязательно) 59 | 60 | Все компоненты игры будут работать локально, кроме функции `loadLevels`, действия которой будут заблокированы политикой безопасности бразуера. 61 | 62 | Один из вариантов обойти это — запустить локальный веб-сервер. 63 | 64 | ##### Локальный сервер на php 65 | 66 | 1. Установить php на компьютер. 67 | 2. Для запуска сервера в папке проекта запустить команду `php -S localhost:3000`. 68 | 3. Для запуска игры откройте в браузере адрес `http://localhost:3000/index.html`. 69 | 4. Для запуска тестов откройте в браузере адрес `http://localhost:3000/test/index.html`. 70 | 71 | ##### Локальный сервер на NodeJS 72 | 73 | 1. Установить NodeJS. 74 | 2. В папке проекта выполнить команду `npm install`. 75 | 3. Для запуска сервера в папке проекта запустить команду `npm start`. 76 | 4. Для запуска игры откройте в браузере адрес `http://localhost:3000/index.html`. 77 | 5. Для запуска тестов откройте в браузере адрес `http://localhost:3000/test/index.html`. 78 | 79 | При использовании NodeJS тесты и игра будут обновляться автоматически при изменении файлов. 80 | 81 | 82 | ### Реализация в песочнице 83 | 84 | #### CodePen 85 | 86 | Для реализации в онлайн-песочнице вам нужно: 87 | 88 | 1. Зарегистрироваться на сервисе [CodePen]. 89 | 2. Открыть заготовку проекта по ссылке: 90 | 91 | https://codepen.io/Netology/pen/QmPgWe?editors=0010 92 | 93 | 3. Нажать кнопку «Fork», тем самым создав свою копию заготовки. 94 | 4. Реализовать код игры, последовательно следуя инструкции, в окне «JS». 95 | 5. Периодически сохраняйте результат, чтобы не потерять изменения. 96 | 6. Отправляйте наставнику на проверку ссылку на ваш пен. 97 | 98 | Инструкция по использованию сервиса CodePen: 99 | https://github.com/netology-code/guides/tree/master/codepen 100 | 101 | #### Repl.it 102 | 103 | 1. Зарегистрироваться на сервисе [Repl.it]. 104 | 2. Создать новую песочницу «HTML, CSS, JS» 105 | 106 | https://repl.it/languages/web_project 107 | 108 | 3. Во вкладке `index.html` поместите следующий код: 109 | 110 | ```html 111 | 112 | 113 | 114 | 115 | 116 | 117 | Document 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | ``` 126 | 127 | 4. Нажмите кнопку «Save». 128 | 5. Реализовать код игры, последовательно следуя инструкции, во вкладке «index.js». 129 | 6. Периодически сохраняйте результат, чтобы не потерять изменения. 130 | 7. Отправляйте наставнику на проверку ссылку на вашу песочницу, которую можно получить по кнопке «Share». 131 | 132 | ## Тестирование 133 | 134 | В файле `./test/index.html` настроена среда автоматизированного тестирования вашего кода. Она проверяет созданные компоненты на соответствие требованиям. И если находит расхождения, сообщает об ошибке. Тем самым, тесты — ваш навигатор, показывающий, какую часть требований в вашем коде вы выполнили, а какую нет. 135 | 136 | По тестам можно осуществлять навигацию. Можно выбрать конкретный компонент или конкретный метод и следить за выполнением только выбранных тестов, не отвлекаясь на другие. 137 | 138 | Также можно отобразить только проваленные тесты или наоборот, только успешные. 139 | 140 | Просто кликайте на соответствующий пункт, чтобы сосредоточиться на нем. 141 | 142 | Процесс реализации можно построить таким образом: 143 | 1. Выбрать компонент или даже метод компонента. 144 | 2. Отфильтровать тесты, оставив только выбранный компонент или его метод. 145 | 3. Реализовать код, который удовлетворит первому проваленному тесту. 146 | 4. Убедиться, что тест помечен как успешный. 147 | 5. Если еще остались проваленные тесты, вернуться к пункту 3. 148 | 149 | Такой подход называется «разработка через тестирование» или TDD. За тем лишь исключением, что тесты уже написаны. 150 | 151 | ## Процесс и порядок реализации 152 | 153 | Для того, чтобы максимально просто и быстро получить базовый рабочий вариант проекта, рекомендуем придерживаться следующего плана разработки: 154 | 155 | 1. Реализовать базовые классы игры: `Vector`, `Actor` и `Level`. 156 | 2. После этого вы уже сможете запустить игру. 157 | ```javascript 158 | const grid = [ 159 | new Array(3), 160 | ['wall', 'wall', 'lava'] 161 | ]; 162 | const level = new Level(grid); 163 | runLevel(level, DOMDisplay); 164 | ``` 165 | 166 | На экране отобразится схема уровня. Узнайте подробнее про функцию `runLevel` и класс `DOMDisplay` ниже. 167 | 168 | 3. Реализуйте `LevelParser`, что позволит вам описывать уровни с помощью текстовой схемы: 169 | ```javascript 170 | const schema = [ 171 | ' ', 172 | ' ', 173 | ' ', 174 | ' ', 175 | ' !xxx', 176 | ' ', 177 | 'xxx! ', 178 | ' ' 179 | ]; 180 | const parser = new LevelParser(); 181 | const level = parser.parse(schema); 182 | runLevel(level, DOMDisplay); 183 | ``` 184 | 4. Реализуйте `Player`, поместите его символ на схему и добавьте словарь при создании парсера: 185 | ```javascript 186 | const schema = [ 187 | ' ', 188 | ' ', 189 | ' ', 190 | ' ', 191 | ' !xxx', 192 | ' @ ', 193 | 'xxx! ', 194 | ' ' 195 | ]; 196 | const actorDict = { 197 | '@': Player 198 | } 199 | const parser = new LevelParser(actorDict); 200 | const level = parser.parse(schema); 201 | runLevel(level, DOMDisplay); 202 | ``` 203 | 5. Реализуйте другие движущиеся объекты игрового поля и помещайте их символы на схему и в словарь парсера. 204 | 6. Реализуйте загрузку уровней с помощью функции `loadLevels` и запуск игры с помощью `runGame`. 205 | 7. Когда игрок пройдет все уровни, используйте функцию `alert`, чтобы сообщить о победе. 206 | 207 | ## Реализованные компоненты, которые необходимо использовать 208 | 209 | ### Класс `DOMDisplay` 210 | 211 | Отвечает за отрисовку в браузере сетки игрового поля и движущихся объектов. Конструктор принимает два аргумента: первый — узел DOM, в котором необходимо отрисовать игровое поле, и уровень `Level`, описывающий игровое поле. 212 | 213 | Непосредственно создавать этот объект не потребуется. Его необходимо передавать вторым аргументом в функцию `runLevel` и третьим аргументом в функцию `runGame`. 214 | 215 | Пример использования: 216 | ```javascript 217 | const schema = [ 218 | ' ', 219 | ' ', 220 | ' = ', 221 | ' ', 222 | ' !xxx', 223 | ' @ ', 224 | 'xxx! ', 225 | ' ' 226 | ]; 227 | const actorDict = { 228 | '@': Player, 229 | '=': HorizontalFireball 230 | } 231 | const parser = new LevelParser(actorDict); 232 | const level = parser.parse(schema); 233 | DOMDisplay(document.body, level); 234 | ``` 235 | 236 | После такого вызова будет отрисовано исходное состояние сетки игрового поля, все движущиеся объекты. Но эта схема будет статичной. 237 | 238 | ### Функция `runLevel` 239 | 240 | Инициализирует процесс регулярной отрисовки текущего состояния игрового поля и обработку событий клавиатуры. 241 | 242 | Принимает два аргумента: уровень (объект класса `Level`) и конструктор объекта, отвечающего за отрисовку. В случае реализации игры в браузере вторым аргументом необходимо использовать класс `DOMDisplay`. 243 | 244 | Функция возвращает промис, который разрешится статусом завершения игры, _строка_. С учетом реализации класса `Level` он может принимать значения `won` или `lost`. 245 | 246 | Пример использования: 247 | ```javascript 248 | const schema = [ 249 | ' ', 250 | ' ', 251 | ' = ', 252 | ' o ', 253 | ' !xxx', 254 | ' @ ', 255 | 'xxx! ', 256 | ' ' 257 | ]; 258 | const actorDict = { 259 | '@': Player, 260 | '=': HorizontalFireball 261 | } 262 | const parser = new LevelParser(actorDict); 263 | const level = parser.parse(schema); 264 | runLevel(level, DOMDisplay) 265 | .then(status => console.log(`Игрок ${status}`)); 266 | ``` 267 | 268 | После вызова такого кода в браузере будет отрисована схема уровня, движущиеся объекты будут перемещаться, и вы сможете управлять игроком с клавиатуры. 269 | 270 | ### Функция `runGame` 271 | 272 | Инициализирует процесс прохождения игры, состоящей из последовательного прохождения нескольких уровней. 273 | 274 | Принимает три аргумента: список схем уровней, _массив_, каждый элемент которого — схема (массив строк); парсер схем, _объект_ `LevelParser`, и конструктор объекта, отвечающего за отрисовку. В случае реализации игры в браузере третьим аргументом необходимо использовать класс `DOMDisplay`. 275 | 276 | Возвращает промис, который разрешится, когда пользователь пройдет все уровни. 277 | 278 | Пример использования: 279 | ```javascript 280 | const schemas = [ 281 | [ 282 | ' ', 283 | ' ', 284 | ' = ', 285 | ' o ', 286 | ' !xxx', 287 | ' @ ', 288 | 'xxx! ', 289 | ' ' 290 | ], 291 | [ 292 | ' v ', 293 | ' v ', 294 | ' v ', 295 | ' o', 296 | ' x', 297 | '@ x ', 298 | 'x ', 299 | ' ' 300 | ] 301 | ]; 302 | const actorDict = { 303 | '@': Player, 304 | 'v': FireRain 305 | } 306 | const parser = new LevelParser(actorDict); 307 | runGame(schemas, parser, DOMDisplay) 308 | .then(() => console.log('Вы выиграли приз!')); 309 | ``` 310 | 311 | Запустит игру из двух уровней, которые необходимо будет пройти последовательно. 312 | 313 | ### Функция `loadLevels` 314 | 315 | Загружает коллекцию уровней. Не принимает аргументов. 316 | 317 | Возвращает промис, который разрешится JSON-строкой, в которой закодирован массив схем уровней. 318 | 319 | ## Компоненты, которые необходимо реализовать 320 | 321 | ### Вектор 322 | 323 | Необходимо реализовать класс `Vector`, который позволит контролировать расположение объектов в двумерном пространстве и управлять их размером и перемещением. 324 | 325 | #### Конструктор 326 | 327 | Принимает два аргумента — координаты по оси X и по оси Y, _числа_, по умолчанию `0`. 328 | 329 | Создает объект со свойствами `x` и `y`, равными переданным в конструктор координатам. 330 | 331 | #### Метод `plus` 332 | 333 | Принимает один аргумент — вектор, _объект_ `Vector`. 334 | 335 | Если передать аргумент другого типа, то бросает исключение `Можно прибавлять к вектору только вектор типа Vector`. 336 | 337 | Создает и возвращает новый _объект_ типа `Vector`, координаты которого будут суммой соответствующих координат суммируемых векторов. 338 | 339 | #### Метод `times` 340 | 341 | Принимает один аргумент — множитель, _число_. 342 | 343 | Создает и возвращает новый _объект_ типа `Vector`, координаты которого будут равны соответствующим координатам исходного вектора, умноженным на множитель. 344 | 345 | #### Пример кода 346 | ```javascript 347 | const start = new Vector(30, 50); 348 | const moveTo = new Vector(5, 10); 349 | const finish = start.plus(moveTo.times(2)); 350 | 351 | console.log(`Исходное расположение: ${start.x}:${start.y}`); 352 | console.log(`Текущее расположение: ${finish.x}:${finish.y}`); 353 | ``` 354 | 355 | Результат выполнения кода: 356 | ``` 357 | Исходное расположение: 30:50 358 | Текущее расположение: 40:70 359 | ``` 360 | 361 | ### Движущийся объект 362 | 363 | Необходимо реализовать класс `Actor`, который позволит контролировать все движущиеся объекты на игровом поле и контролировать их пересечение. 364 | 365 | #### Конструктор 366 | 367 | Принимает три аргумента: расположение, _объект_ типа `Vector`, размер, тоже _объект_ типа `Vector` и скорость, тоже _объект_ типа `Vector`. Все аргументы необязательные. По умолчанию создается объект с координатами 0:0, размером 1x1 и скоростью 0:0. 368 | 369 | Если в качестве первого, второго или третьего аргумента передать не объект типа `Vector`, то конструктор должен бросить исключение. 370 | 371 | #### Свойства 372 | 373 | Должно быть определено свойство `pos`, в котором размещен `Vector`. 374 | 375 | Должно быть определено свойство `size`, в котором размещен `Vector`. 376 | 377 | Должно быть определено свойство `speed`, в котором размещен `Vector`. 378 | 379 | Должен быть определен метод `act`, который ничего не делает. 380 | 381 | Должны быть определены свойства только для чтения `left`, `top`, `right`, `bottom`, в которых установлены границы объекта по осям X и Y с учетом его расположения и размера. 382 | 383 | Должен иметь свойство `type` — строку со значением `actor`, только для чтения. 384 | 385 | #### Метод `isIntersect` 386 | 387 | Метод проверяет, пересекается ли текущий объект с переданным объектом, и если да, возвращает `true`, иначе – `false`. 388 | 389 | Принимает один аргумент — движущийся объект типа `Actor`. Если передать аргумент другого типа или вызвать без аргументов, то метод бросает исключение. 390 | 391 | Если передать в качестве аргумента этот же объект, то всегда возвращает `false`. Объект не пересекается сам с собой. 392 | 393 | Объекты, имеющие смежные границы, не пересекаются. 394 | 395 | #### Пример кода 396 | ```javascript 397 | const items = new Map(); 398 | const player = new Actor(); 399 | items.set('Игрок', player); 400 | items.set('Первая монета', new Actor(new Vector(10, 10))); 401 | items.set('Вторая монета', new Actor(new Vector(15, 5))); 402 | 403 | function position(item) { 404 | return ['left', 'top', 'right', 'bottom'] 405 | .map(side => `${side}: ${item[side]}`) 406 | .join(', '); 407 | } 408 | 409 | function movePlayer(x, y) { 410 | player.pos = player.pos.plus(new Vector(x, y)); 411 | } 412 | 413 | function status(item, title) { 414 | console.log(`${title}: ${position(item)}`); 415 | if (player.isIntersect(item)) { 416 | console.log(`Игрок подобрал ${title}`); 417 | } 418 | } 419 | 420 | items.forEach(status); 421 | movePlayer(10, 10); 422 | items.forEach(status); 423 | movePlayer(5, -5); 424 | items.forEach(status); 425 | ``` 426 | 427 | Результат работы примера: 428 | ``` 429 | Игрок: left: 0, top: 0, right: 1, bottom: 1 430 | Первая монета: left: 10, top: 10, right: 11, bottom: 11 431 | Вторая монета: left: 15, top: 5, right: 16, bottom: 6 432 | Игрок: left: 10, top: 10, right: 11, bottom: 11 433 | Первая монета: left: 10, top: 10, right: 11, bottom: 11 434 | Игрок подобрал Первая монета 435 | Вторая монета: left: 15, top: 5, right: 16, bottom: 6 436 | Игрок: left: 15, top: 5, right: 16, bottom: 6 437 | Первая монета: left: 10, top: 10, right: 11, bottom: 11 438 | Вторая монета: left: 15, top: 5, right: 16, bottom: 6 439 | Игрок подобрал Вторая монета 440 | ``` 441 | 442 | ### Игровое поле 443 | 444 | Объекты класса `Level` реализуют схему игрового поля конкретного уровня, контролируют все движущиеся объекты на нём и реализуют логику игры. Уровень представляет собой координатное поле, имеющее фиксированную ширину и высоту. 445 | 446 | Сетка уровня представляет собой координатное двумерное поле, представленное двумерным массивом. Первый массив — строки игрового поля; индекс этого массива соответствует координате Y на игровом поле. Элемент с индексом `5` соответствует строке с координатой Y, равной `5`. Вложенные массивы, расположенные в элементах массива строк, представляют ячейки поля. Индекс этих массивов соответствует координате X. Например, элемент с индексом `10`, соответствует ячейке с координатой X, равной `10`. 447 | 448 | Так как `grid` — это двумерный массив, представляющий сетку игрового поля, то, чтобы узнать, что находится в ячейке с координатами X=10 и Y=5 (10:5), необходимо получить значение `grid[5][10]`. Если значение этого элемента равно `undefined`, то эта ячейка пуста. Иначе там будет строка, описывающая препятствие. Например, `wall` — для стены и `lava` — для лавы. Отсюда вытекает следующий факт: все препятствия имеют целочисленные размеры и координаты. 449 | 450 | #### Конструктор 451 | 452 | Принимает два аргумента: сетку игрового поля с препятствиями, _массив массивов строк_, и список движущихся объектов, _массив объектов_ `Actor`. Оба аргумента необязательные. 453 | 454 | #### Свойства 455 | 456 | Имеет свойство `grid` — сетку игрового поля. Двумерный массив строк. 457 | 458 | Имеет свойство `actors` — список движущихся объектов игрового поля, массив объектов `Actor`. 459 | 460 | Имеет свойство `player` — движущийся объект, тип которого — свойство `type` — равно `player`. Игорок передаётся с остальными движущимися объектами. 461 | 462 | Имеет свойство `height` — высоту игрового поля, равное числу строк в сетке из первого аргумента. 463 | 464 | Имеет свойство `width` — ширину игрового поля, равное числу ячеек в строке сетки из первого аргумента. При этом, если в разных строках разное число ячеек, то `width` будет равно максимальному количеству ячеек в строке. 465 | 466 | Имеет свойство `status` — состояние прохождения уровня, равное `null` после создания. 467 | 468 | Имеет свойство `finishDelay` — таймаут после окончания игры, равен `1` после создания. Необходим, чтобы после выигрыша или проигрыша игра не завершалась мгновенно. 469 | 470 | #### Метод `isFinished` 471 | 472 | Определяет, завершен ли уровень. Не принимает аргументов. 473 | 474 | Возвращает `true`, если свойство `status` не равно `null` и `finishDelay` меньше нуля. 475 | 476 | #### Метод `actorAt` 477 | 478 | Определяет, расположен ли какой-то другой движущийся объект в переданной позиции, и если да, вернёт этот объект. 479 | 480 | Принимает один аргумент — движущийся объект, `Actor`. Если не передать аргумент или передать не объект `Actor`, метод должен бросить исключение. 481 | 482 | Возвращает `undefined`, если переданный движущийся объект не пересекается ни с одним объектом на игровом поле. 483 | 484 | Возвращает объект `Actor`, если переданный объект пересекается с ним на игровом поле. Если пересекается с несколькими объектами, вернет первый. 485 | 486 | #### Метод `obstacleAt` 487 | 488 | Аналогично методу `actorAt` определяет, нет ли препятствия в указанном месте. Также этот метод контролирует выход объекта за границы игрового поля. 489 | 490 | Так как движущиеся объекты не могут двигаться сквозь стены, то метод принимает два аргумента: положение, куда собираемся передвинуть объект, _вектор_ `Vector`, и размер этого объекта, тоже _вектор_ `Vector`. Если первым и вторым аргументом передать не `Vector`, то метод бросает исключение. 491 | 492 | Вернет строку, соответствующую препятствию из сетки игрового поля, пересекающему область, описанную двумя переданными векторами, либо `undefined`, если в этой области препятствий нет. 493 | 494 | Если описанная двумя векторами область выходит за пределы игрового поля, то метод вернет строку `lava`, если область выступает снизу. И вернет `wall` в остальных случаях. Будем считать, что игровое поле слева, сверху и справа огорожено стеной и снизу у него смертельная лава. 495 | 496 | #### Метод `removeActor` 497 | 498 | Метод удаляет переданный объект с игрового поля. Если такого объекта на игровом поле нет, не делает ничего. 499 | 500 | Принимает один аргумент, объект `Actor`. Находит и удаляет его. 501 | 502 | #### Метод `noMoreActors` 503 | 504 | Определяет, остались ли еще объекты переданного типа на игровом поле. 505 | 506 | Принимает один аргумент — тип движущегося объекта, _строка_. 507 | 508 | Возвращает `true`, если на игровом поле нет объектов этого типа (свойство `type`). Иначе возвращает `false`. 509 | 510 | #### Метод `playerTouched` 511 | 512 | Один из ключевых методов, определяющий логику игры. Меняет состояние игрового поля при касании игроком каких-либо объектов или препятствий. 513 | 514 | Если состояние игры уже отлично от `null`, то не делаем ничего, игра уже и так завершилась. 515 | 516 | Принимает два аргумента. Тип препятствия или объекта, _строка_. Движущийся объект, которого коснулся игрок, — объект типа `Actor`, необязательный аргумент. 517 | 518 | Если первым аргументом передать строку `lava` или `fireball`, то меняем статус игры на `lost` (свойство `status`). Игрок проигрывает при касании лавы или шаровой молнии. 519 | 520 | Если первым аргументом передать строку `coin`, а вторым — объект монеты, то необходимо удалить эту монету с игрового поля. Если при этом на игровом поле не осталось больше монет, то меняем статус игры на `won`. Игрок побеждает, когда собирает все монеты на уровне. Отсюда вытекает факт, что уровень без монет пройти невозможно. 521 | 522 | #### Пример кода 523 | 524 | ```javascript 525 | const grid = [ 526 | [undefined, undefined], 527 | ['wall', 'wall'] 528 | ]; 529 | 530 | function MyCoin(title) { 531 | this.type = 'coin'; 532 | this.title = title; 533 | } 534 | MyCoin.prototype = Object.create(Actor); 535 | MyCoin.constructor = MyCoin; 536 | 537 | const goldCoin = new MyCoin('Золото'); 538 | const bronzeCoin = new MyCoin('Бронза'); 539 | const player = new Actor(); 540 | const fireball = new Actor(); 541 | 542 | const level = new Level(grid, [ goldCoin, bronzeCoin, player, fireball ]); 543 | 544 | level.playerTouched('coin', goldCoin); 545 | level.playerTouched('coin', bronzeCoin); 546 | 547 | if (level.noMoreActors('coin')) { 548 | console.log('Все монеты собраны'); 549 | console.log(`Статус игры: ${level.status}`); 550 | } 551 | 552 | const obstacle = level.obstacleAt(new Vector(1, 1), player.size); 553 | if (obstacle) { 554 | console.log(`На пути препятствие: ${obstacle}`); 555 | } 556 | 557 | const otherActor = level.actorAt(player); 558 | if (otherActor === fireball) { 559 | console.log('Пользователь столкнулся с шаровой молнией'); 560 | } 561 | ``` 562 | 563 | Результат выполнения: 564 | ``` 565 | Все монеты собраны 566 | Статус игры: won 567 | На пути препятствие: wall 568 | Пользователь столкнулся с шаровой молнией 569 | ``` 570 | 571 | ### Парсер уровня 572 | 573 | Объект класса `LevelParser` позволяет создать игровое поле `Level` из массива строк по следующему принципу: 574 | * Каждый элемент массива соответствует строке в сетке уровня. 575 | * Каждый символ строки соответствует ячейке в сетке уровня. 576 | * Символ определяет тип объекта или препятствия. 577 | * Индекс строки и индекс символа определяют исходные координаты объекта или координаты препятствия. 578 | 579 | Символы и соответствующие им препятствия и объекты игрового поля: 580 | * **x** — стена, препятствие 581 | * **!** — лава, препятствие 582 | * **@** — игрок, объект 583 | * **o** — монетка, объект 584 | * **=** — движущаяся горизонтально шаровая молния, объект 585 | * **|** — движущаяся вертикально шаровая молния, объект 586 | * **v** — огненный дождь, объект 587 | 588 | > Обратите внимание, что тут слово «символ» означает букву, цифру или знак, которые используются в строках, а не тип данных `Symbol`. 589 | 590 | #### Конструктор 591 | 592 | Принимает один аргумент — словарь движущихся объектов игрового поля, _объект_, ключами которого являются символы из текстового представления уровня, а значениями — конструкторы, с помощью которых можно создать новый объект. 593 | 594 | #### Метод `actorFromSymbol` 595 | 596 | Принимает символ, _строка_. Возвращает конструктор объекта по его символу, используя словарь. Если в словаре не нашлось ключа с таким символом, вернет `undefined`. 597 | 598 | #### Метод `obstacleFromSymbol` 599 | 600 | Аналогично принимает символ, _строка_. Возвращает строку, соответствующую символу препятствия. Если символу нет соответствующего препятствия, то вернет `undefined`. 601 | 602 | Вернет `wall`, если передать `x`. 603 | 604 | Вернет `lava`, если передать `!`. 605 | 606 | Вернет `undefined`, если передать любой другой символ. 607 | 608 | #### Метод `createGrid` 609 | 610 | Принимает массив строк и преобразует его в массив массивов, в ячейках которого хранится либо строка, соответствующая препятствию, либо `undefined`. 611 | 612 | Движущиеся объекты не должны присутствовать на сетке. 613 | 614 | #### Метод `createActors` 615 | 616 | Принимает массив строк и преобразует его в массив движущихся объектов, используя для их создания классы из словаря. 617 | 618 | Количество движущихся объектов в результирующем массиве должно быть равно количеству символов объектов в массиве строк. 619 | 620 | Каждый объект должен быть создан с использованием вектора, определяющего его положение с учетом координат, полученных на основе индекса строки в массиве (Y) и индекса символа в строке (X). 621 | 622 | Для создания объекта должен быть использован класс из словаря, соответствующий символу. При этом, если этот класс не является наследником `Actor`, то такой символ игнорируется, и объект не создается. 623 | 624 | #### Метод `parse` 625 | 626 | Принимает массив строк, создает и возвращает игровое поле, заполненное препятствиями и движущимися объектами, полученными на основе символов и словаря. 627 | 628 | #### Пример использования 629 | 630 | ```javascript 631 | const plan = [ 632 | ' @ ', 633 | 'x!x' 634 | ]; 635 | 636 | const actorsDict = Object.create(null); 637 | actorsDict['@'] = Actor; 638 | 639 | const parser = new LevelParser(actorsDict); 640 | const level = parser.parse(plan); 641 | 642 | level.grid.forEach((line, y) => { 643 | line.forEach((cell, x) => console.log(`(${x}:${y}) ${cell}`)); 644 | }); 645 | 646 | level.actors.forEach(actor => console.log(`(${actor.pos.x}:${actor.pos.y}) ${actor.type}`)); 647 | ``` 648 | 649 | Результат выполнения кода: 650 | ``` 651 | (0:0) undefined 652 | (1:0) undefined 653 | (2:0) undefined 654 | (0:1) wall 655 | (1:1) lava 656 | (2:1) wall 657 | (1:0) actor 658 | ``` 659 | 660 | ### Шаровая молния 661 | 662 | Класс `Fireball` станет прототипом для движущихся опасностей на игровом поле. Он должен наследовать весь функционал движущегося объекта `Actor`. 663 | 664 | #### Конструктор 665 | 666 | Принимает два аргумента: координаты, _объект_ `Vector` и скорость, тоже _объект_ `Vector`. Оба аргумента необязательные. По умолчанию создается объект с координатами `0:0` и скоростью `0:0`. 667 | 668 | #### Свойства 669 | 670 | Созданный объект должен иметь свойство `type` со значением `fireball`. Это свойство только для чтения. 671 | 672 | Также должен иметь размер `1:1` в свойстве `size`, _объект_ `Vector`. 673 | 674 | #### Метод `getNextPosition` 675 | 676 | Создает и возвращает вектор `Vector` следующей позиции шаровой молнии. Это функция времени. И как в школьной задаче, новая позиция — это текущая позиция плюс скорость, умноженная на время. И так по каждой из осей. 677 | 678 | Принимает один аргумент, время, _число_. Аргумент необязательный, по умолчанию равен `1`. 679 | 680 | #### Метод `handleObstacle` 681 | 682 | Обрабатывает столкновение молнии с препятствием. Не принимает аргументов. Ничего не возвращает. 683 | 684 | Меняет вектор скорости на противоположный. Если он был `5:5`, то после должен стать `-5:-5`. 685 | 686 | #### Метод `act` 687 | 688 | Обновляет состояние движущегося объекта. 689 | 690 | Принимает два аргумента. Первый — время, _число_, второй — игровое поле, _объект_ `Level`. 691 | 692 | Метод ничего не возвращает. Но должен выполнить следующие действия: 693 | 1. Получить следующую позицию, используя время. 694 | 2. Выяснить, не пересечется ли в следующей позиции объект с каким-либо препятствием. Пересечения с другими движущимися объектами учитывать не нужно. 695 | 3. Если нет, обновить текущую позицию объекта. 696 | 4. Если объект пересекается с препятствием, то необходимо обработать это событие. При этом текущее положение остается прежним. 697 | 698 | #### Пример использования 699 | 700 | ```javascript 701 | const time = 5; 702 | const speed = new Vector(1, 0); 703 | const position = new Vector(5, 5); 704 | 705 | const ball = new Fireball(position, speed); 706 | 707 | const nextPosition = ball.getNextPosition(time); 708 | console.log(`Новая позиция: ${nextPosition.x}: ${nextPosition.y}`); 709 | 710 | ball.handleObstacle(); 711 | console.log(`Текущая скорость: ${ball.speed.x}: ${ball.speed.y}`); 712 | ``` 713 | 714 | Результат работы кода: 715 | ``` 716 | Новая позиция: 10: 5 717 | Текущая скорость: -1: 0 718 | ``` 719 | 720 | ### Горизонтальная шаровая молния 721 | 722 | Вам необходимо самостоятельно реализовать класс `HorizontalFireball`. Он будет представлять собой объект, который движется по горизонтали со скоростью `2` и при столкновении с препятствием движется в обратную сторону. 723 | 724 | Конструктор должен принимать один аргумент — координаты текущего положения, _объект_ `Vector`. И создавать объект размером `1:1` и скоростью, равной `2` по оси X. 725 | 726 | ### Вертикальная шаровая молния 727 | 728 | Вам необходимо самостоятельно реализовать класс `VerticalFireball`. Он будет представлять собой объект, который движется по вертикали со скоростью `2` и при столкновении с препятствием движется в обратную сторону. 729 | 730 | Конструктор должен принимать один аргумент: координаты текущего положения, _объект_ `Vector`. И создавать объект размером `1:1` и скоростью, равной `2` по оси Y. 731 | 732 | ### Огненный дождь 733 | 734 | Вам необходимо самостоятельно реализовать класс `FireRain`. Он будет представлять собой объект, который движется по вертикали со скоростью `3` и при столкновении с препятствием начинает движение в том же направлении из исходного положения, которое задано при создании. 735 | 736 | Конструктор должен принимать один аргумент — координаты текущего положения, _объект_ `Vector`. И создавать объект размером `1:1` и скоростью, равной `3` по оси Y. 737 | 738 | ### Монета 739 | 740 | Класс `Coin` реализует поведение монетки на игровом поле. Чтобы привлекать к себе внимание, монетки должны постоянно подпрыгивать в рамках своей ячейки. Класс должен наследовать весь функционал движущегося объекта `Actor`. 741 | 742 | #### Конструктор 743 | 744 | Принимает один аргумент — координаты положения на игровом поле, _объект_ `Vector`. 745 | 746 | Созданный объект должен иметь размер `0,6:0,6`. А его реальные координаты должны отличаться от тех, что переданы в конструктор, на вектор `0,2:0,1`. 747 | 748 | #### Свойства 749 | 750 | Свойство `type` созданного объекта должно иметь значение `coin`. 751 | 752 | Также объект должен иметь следующие свойства: 753 | * Скорость подпрыгивания, `springSpeed`, равная `8`; 754 | * Радиус подпрыгивания, `springDist`, равен `0.07`; 755 | * Фаза подпрыгивания, `spring`, случайное число от `0` до `2π`. 756 | 757 | #### Метод `updateSpring` 758 | 759 | Обновляет фазу подпрыгивания. Это функция времени. 760 | 761 | Принимает один аргумент — время, _число_, по умолчанию `1`. 762 | 763 | Ничего не возвращает. Обновляет текущую фазу `spring`, увеличив её на скорость `springSpeed`, умноженную на время. 764 | 765 | #### Метод `getSpringVector` 766 | 767 | Создает и возвращает вектор подпрыгивания. Не принимает аргументов. 768 | 769 | Так как подпрыгивание происходит только по оси Y, то координата X вектора всегда равна нулю. 770 | 771 | Координата Y вектора равна синусу текущей фазы, умноженному на радиус. 772 | 773 | #### Метод `getNextPosition` 774 | 775 | Обновляет текущую фазу, создает и возвращает вектор новой позиции монетки. 776 | 777 | Принимает один аргумент — время, _число_, по умолчанию `1`. 778 | 779 | Новый вектор равен базовому вектору положения, увеличенному на вектор подпрыгивания. Увеличивать нужно именно базовый вектор положения, который получен в конструкторе, а не текущий. 780 | 781 | #### Метод `act` 782 | 783 | Принимает один аргумент — время. Получает новую позицию объекта и задает её как текущую. Ничего не возвращает. 784 | 785 | ### Игрок 786 | 787 | Класс `Player` содержит базовый функционал движущегося объекта, который представляет игрока на игровом поле. Должен наследовать возможности `Actor`. 788 | 789 | #### Конструктор 790 | 791 | Принимает один аргумент — координаты положения на игровом поле, _объект_ `Vector`. 792 | 793 | Созданный объект, реальное положение которого отличается от того, что передано в конструктор, на вектор `0:-0,5`. Имеет размер `0,8:1,5`. И скорость `0:0`. 794 | 795 | #### Свойства 796 | 797 | Имеет свойство `type`, равное `player`. 798 | 799 | [bitbucket]: https://bitbucket.org/ 800 | [github]: https://github.com/ 801 | [codepen]: https://codepen.io/ 802 | [Repl.it]: https://repl.it/ 803 | 804 | ### Как правильно задавать вопросы дипломному руководителю? 805 | Что следует делать, чтобы все получилось: 806 | 1. Попробовать найти ответ сначала самому в интернете. Ведь, именно это скилл поиска ответов пригодится тебе на первой работе. И только после этого спрашивать дипломного руководителя 807 | 2. В одном вопросе должна быть заложена одна проблема 808 | 3. По возможности, прикреплять к вопросу скриншоты и стрелочкой показывать где не получается. Программу для этого можно скачать здесь https://app.prntscr.com/ru/ 809 | 4. По возможности, задавать вопросы в комментариях к коду. 810 | Начинать работу над дипломом как можно раньше! Чтобы было больше времени на правки. 811 | 5. Делать диплом по-частям, а не все сразу. Иначе, есть шанс, что нужно будет все переделывать :) 812 | Что следует делать, чтобы ничего не получилось: 813 | 1. Писать вопросы вида “Ничего не работает. Не запускается. Всё сломалось.” 814 | 2. Откладывать диплом на потом. 815 | 3. Ждать ответ на свой вопрос моментально. Дипломные руководители - работающие разработчики, которые занимаются, кроме преподавания, своими проектами. Их время ограничено, поэтому постарайтесь задавать правильные вопросы, чтобы получать быстрые ответы! 816 | 817 | 818 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | hidden { 2 | display: none; 3 | } 4 | 5 | .background { 6 | background: rgb(52, 166, 251); 7 | table-layout: fixed; 8 | border-spacing: 0; 9 | } 10 | 11 | .background td { 12 | padding: 0; 13 | } 14 | 15 | .fireball { 16 | background: rgb(255, 100, 100); 17 | } 18 | 19 | .lava { 20 | background: rgb(255, 100, 100); 21 | } 22 | 23 | .elevator { 24 | background: rgb(229, 229, 229); 25 | } 26 | 27 | .wall { 28 | background: white; 29 | } 30 | 31 | .actor { 32 | position: absolute; 33 | } 34 | 35 | .coin { 36 | background: rgb(241, 229, 89); 37 | } 38 | 39 | .player { 40 | background: rgb(64, 64, 64); 41 | } 42 | 43 | .lost .player { 44 | background: rgb(160, 64, 64); 45 | } 46 | 47 | .won .player { 48 | box-shadow: -4px -7px 8px white, 4px -7px 8px white; 49 | } 50 | 51 | .game { 52 | overflow: hidden; 53 | max-width: 600px; 54 | max-height: 450px; 55 | position: relative; 56 | } 57 | -------------------------------------------------------------------------------- /game.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function loadLevels() { 4 | return new Promise((done, fail) => { 5 | const xhr = new XMLHttpRequest(); 6 | let url = './levels.json'; 7 | if (location.hostname !== 'localhost') { 8 | url = 'https://neto-api.herokuapp.com/js/diplom/levels.json'; 9 | } 10 | xhr.open('GET', url); 11 | xhr.addEventListener('error', e => fail(xhr)); 12 | xhr.addEventListener('load', e => { 13 | if (xhr.status !== 200) { 14 | fail(xhr); 15 | } 16 | done(xhr.responseText); 17 | }); 18 | xhr.send(); 19 | }); 20 | } 21 | 22 | const scale = 30; 23 | const maxStep = 0.05; 24 | const wobbleSpeed = 8, wobbleDist = 0.07; 25 | const playerXSpeed = 7; 26 | const gravity = 30; 27 | const jumpSpeed = 17; 28 | 29 | function elt(name, className) { 30 | var elt = document.createElement(name); 31 | if (className) elt.className = className; 32 | return elt; 33 | } 34 | 35 | class DOMDisplay { 36 | constructor(parent, level) { 37 | this.wrap = parent.appendChild(elt("div", "game")); 38 | this.wrap.setAttribute('autofocus', true) 39 | this.level = level; 40 | 41 | this.actorMap = new Map(); 42 | this.wrap.appendChild(this.drawBackground()); 43 | this.actorLayer = this.wrap.appendChild(this.drawActors()); 44 | this.drawFrame(); 45 | } 46 | 47 | drawBackground() { 48 | var table = elt("table", "background"); 49 | table.style.width = this.level.width * scale + "px"; 50 | this.level.grid.forEach(function(row) { 51 | var rowElt = table.appendChild(elt("tr")); 52 | rowElt.style.height = scale + "px"; 53 | row.forEach(function(type) { 54 | rowElt.appendChild(elt("td", type)); 55 | }); 56 | }); 57 | return table; 58 | } 59 | 60 | drawActor(actor) { 61 | return elt('div', `actor ${actor.type}`); 62 | } 63 | 64 | updateActor(actor, rect) { 65 | rect.style.width = actor.size.x * scale + "px"; 66 | rect.style.height = actor.size.y * scale + "px"; 67 | rect.style.left = actor.pos.x * scale + "px"; 68 | rect.style.top = actor.pos.y * scale + "px"; 69 | } 70 | 71 | drawActors() { 72 | var wrap = elt('div'); 73 | this.level.actors.forEach(actor => { 74 | const rect = wrap.appendChild(this.drawActor(actor)); 75 | this.actorMap.set(actor, rect); 76 | }); 77 | return wrap; 78 | } 79 | 80 | updateActors() { 81 | for (const [actor, rect] of this.actorMap) { 82 | if (this.level.actors.includes(actor)) { 83 | this.updateActor(actor, rect); 84 | } else { 85 | this.actorMap.delete(actor); 86 | rect.parentElement.removeChild(rect); 87 | } 88 | } 89 | } 90 | 91 | drawFrame() { 92 | this.updateActors(); 93 | 94 | this.wrap.className = "game " + (this.level.status || ""); 95 | this.scrollPlayerIntoView(); 96 | } 97 | 98 | scrollPlayerIntoView() { 99 | var width = this.wrap.clientWidth; 100 | var height = this.wrap.clientHeight; 101 | var margin = width / 3; 102 | 103 | // The viewport 104 | var left = this.wrap.scrollLeft, right = left + width; 105 | var top = this.wrap.scrollTop, bottom = top + height; 106 | 107 | var player = this.level.player; 108 | if (!player) { 109 | return; 110 | } 111 | var center = player.pos.plus(player.size.times(0.5)) 112 | .times(scale); 113 | 114 | if (center.x < left + margin) 115 | this.wrap.scrollLeft = center.x - margin; 116 | else if (center.x > right - margin) 117 | this.wrap.scrollLeft = center.x + margin - width; 118 | if (center.y < top + margin) 119 | this.wrap.scrollTop = center.y - margin; 120 | else if (center.y > bottom - margin) 121 | this.wrap.scrollTop = center.y + margin - height; 122 | } 123 | 124 | clear() { 125 | this.wrap.parentNode.removeChild(this.wrap); 126 | } 127 | } 128 | 129 | var arrowCodes = {37: "left", 38: "up", 39: "right"}; 130 | 131 | function trackKeys(codes) { 132 | var pressed = Object.create(null); 133 | function handler(event) { 134 | if (codes.hasOwnProperty(event.keyCode)) { 135 | var down = event.type == "keydown"; 136 | pressed[codes[event.keyCode]] = down; 137 | event.preventDefault(); 138 | } 139 | } 140 | addEventListener("keydown", handler); 141 | addEventListener("keyup", handler); 142 | return pressed; 143 | } 144 | 145 | function runAnimation(frameFunc) { 146 | var lastTime = null; 147 | function frame(time) { 148 | var stop = false; 149 | if (lastTime != null) { 150 | var timeStep = Math.min(time - lastTime, 100) / 1000; 151 | stop = frameFunc(timeStep) === false; 152 | } 153 | lastTime = time; 154 | if (!stop) { 155 | requestAnimationFrame(frame); 156 | } 157 | } 158 | requestAnimationFrame(frame); 159 | } 160 | 161 | function runLevel(level, Display) { 162 | initGameObjects(); 163 | return new Promise(done => { 164 | var arrows = trackKeys(arrowCodes); 165 | var display = new Display(document.body, level); 166 | runAnimation(step => { 167 | level.act(step, arrows); 168 | display.drawFrame(step); 169 | if (level.isFinished()) { 170 | display.clear(); 171 | done(level.status); 172 | return false; 173 | } 174 | }); 175 | }); 176 | } 177 | 178 | function initGameObjects() { 179 | if (initGameObjects.isInit) { 180 | return; 181 | } 182 | 183 | initGameObjects.isInit = true; 184 | 185 | Level.prototype.act = function(step, keys) { 186 | if (this.status !== null) { 187 | this.finishDelay -= step; 188 | } 189 | 190 | while (step > 0) { 191 | var thisStep = Math.min(step, maxStep); 192 | this.actors.forEach(actor => { 193 | actor.act(thisStep, this, keys); 194 | }); 195 | 196 | if (this.status === 'lost') { 197 | this.player.pos.y += thisStep; 198 | this.player.size.y -= thisStep; 199 | } 200 | 201 | step -= thisStep; 202 | } 203 | }; 204 | 205 | Player.prototype.handleObstacle = function (obstacle) { 206 | if (this.wontJump) { 207 | this.speed.y = -jumpSpeed; 208 | } else { 209 | this.speed.y = 0; 210 | } 211 | }; 212 | 213 | Player.prototype.move = function (motion, level) { 214 | var newPos = this.pos.plus(motion); 215 | var obstacle = level.obstacleAt(newPos, this.size); 216 | if (obstacle) { 217 | level.playerTouched(obstacle); 218 | this.handleObstacle(obstacle); 219 | } else { 220 | this.pos = newPos; 221 | } 222 | }; 223 | 224 | Player.prototype.moveX = function (step, level, keys) { 225 | this.speed.x = 0; 226 | if (keys.left) this.speed.x -= playerXSpeed; 227 | if (keys.right) this.speed.x += playerXSpeed; 228 | 229 | var motion = new Vector(this.speed.x, 0).times(step); 230 | this.move(motion, level); 231 | }; 232 | 233 | Player.prototype.moveY = function (step, level, keys) { 234 | this.speed.y += step * gravity; 235 | this.wontJump = keys.up && this.speed.y > 0; 236 | 237 | var motion = new Vector(0, this.speed.y).times(step); 238 | this.move(motion, level); 239 | }; 240 | 241 | Player.prototype.act = function (step, level, keys) { 242 | this.moveX(step, level, keys); 243 | this.moveY(step, level, keys); 244 | 245 | var otherActor = level.actorAt(this); 246 | if (otherActor) { 247 | level.playerTouched(otherActor.type, otherActor); 248 | } 249 | }; 250 | } 251 | 252 | function runGame(plans, Parser, Display) { 253 | return new Promise(done => { 254 | function startLevel(n) { 255 | runLevel(Parser.parse(plans[n]), Display) 256 | .then(status => { 257 | if (status == "lost") { 258 | startLevel(n); 259 | } else if (n < plans.length - 1) { 260 | startLevel(n + 1); 261 | } else { 262 | done(); 263 | } 264 | }); 265 | } 266 | startLevel(0); 267 | }); 268 | } 269 | 270 | function rand(max = 10, min = 0) { 271 | return Math.floor(Math.random() * (max - min + 1)) + min; 272 | } 273 | -------------------------------------------------------------------------------- /levels.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | " v ", 4 | " ", 5 | " ", 6 | " ", 7 | " ", 8 | " | w ", 9 | " o o ", 10 | " x = x ", 11 | " x o o x ", 12 | " x @ * xxxxx x ", 13 | " xxxxx x ", 14 | " x!!!!!!!!!!!!!x ", 15 | " xxxxxxxxxxxxxxx ", 16 | " " 17 | ], 18 | [ 19 | " v ", 20 | " ", 21 | " ", 22 | " ", 23 | " ", 24 | " | ", 25 | " o o ", 26 | " x = x ", 27 | " x o o x ", 28 | " x @ xxxxx x ", 29 | " xxxxx x ", 30 | " x!!!!!!!!!!!!!x ", 31 | " xxxxxxxxxxxxxxx ", 32 | " " 33 | ], 34 | [ 35 | " | | ", 36 | " ", 37 | " ", 38 | " ", 39 | " ", 40 | " ", 41 | " ", 42 | " ", 43 | " ", 44 | " | ", 45 | " ", 46 | " = | ", 47 | " @ | o o ", 48 | "xxxxxxxxx!!!!!!!xxxxxxx", 49 | " " 50 | ], 51 | [ 52 | " ", 53 | " ", 54 | " ", 55 | " o ", 56 | " x | x!!x= ", 57 | " x ", 58 | " x", 59 | " ", 60 | " ", 61 | " ", 62 | " xxx ", 63 | " ", 64 | " ", 65 | " xxx | ", 66 | " ", 67 | " @ ", 68 | "xxx ", 69 | " " 70 | ], [ 71 | " v v", 72 | " ", 73 | " !o! ", 74 | " ", 75 | " ", 76 | " ", 77 | " ", 78 | " xxx ", 79 | " o ", 80 | " = ", 81 | " @ ", 82 | " xxxx ", 83 | " | ", 84 | " xxx x", 85 | " ", 86 | " ! ", 87 | " ", 88 | " ", 89 | " o x ", 90 | " x x ", 91 | " x ", 92 | " x ", 93 | " xx ", 94 | " " 95 | ] 96 | ] 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-game", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "browser-sync start --server --files \"**/*.*\"", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "browser-sync": "^2.18.8" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /res/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netology-code/js-game/d475e6c27a45fa22053d00ba9a78653661822a15/res/preview.png -------------------------------------------------------------------------------- /test/actor.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс Actor', () => { 4 | let position, size; 5 | 6 | beforeEach(() => { 7 | position = new Vector(30, 50); 8 | size = new Vector(5, 5); 9 | }); 10 | 11 | describe('Конструктор new Actor()', () => { 12 | it('Создает объект со свойством pos, который является вектором', () => { 13 | const player = new Actor(); 14 | 15 | expect(player.pos).is.instanceof(Vector); 16 | }); 17 | 18 | it('Создает объект со свойством size, который является вектором', () => { 19 | const player = new Actor(); 20 | 21 | expect(player.size).is.instanceof(Vector); 22 | }); 23 | 24 | it('Создает объект со свойством speed, который является вектором', () => { 25 | const player = new Actor(); 26 | 27 | expect(player.speed).is.instanceof(Vector); 28 | }); 29 | 30 | it('Создает объект со свойством type, который является строкой', () => { 31 | const player = new Actor(); 32 | 33 | expect(player.type).to.be.a('string'); 34 | }); 35 | 36 | it('Создает объект с методом act', () => { 37 | const player = new Actor(); 38 | 39 | expect(player.act).is.instanceof(Function); 40 | }); 41 | 42 | it('По умолчанию создается объект, расположенный в точке 0:0', () => { 43 | const player = new Actor(); 44 | 45 | expect(player.pos).is.eql(new Vector(0, 0)); 46 | }); 47 | 48 | it('По умолчанию создается объект размером 1x1', () => { 49 | const player = new Actor(); 50 | 51 | expect(player.size).is.eql(new Vector(1, 1)); 52 | }); 53 | 54 | it('По умолчанию создается объект со скоростью 0:0', () => { 55 | const player = new Actor(); 56 | 57 | expect(player.speed).is.eql(new Vector(0, 0)); 58 | }); 59 | 60 | it('По умолчанию создается объект со свойством type равным actor', () => { 61 | const player = new Actor(); 62 | 63 | expect(player.type).to.equal('actor'); 64 | }); 65 | 66 | it('Свойство type нельзя изменить', () => { 67 | const player = new Actor(); 68 | 69 | function fn() { 70 | player.type = 'player'; 71 | } 72 | 73 | expect(fn).to.throw(Error); 74 | }); 75 | 76 | it('Создает объект в заданном расположении, если передать вектор первым аргументом', () => { 77 | const player = new Actor(position); 78 | 79 | expect(player.pos).is.equal(position); 80 | }); 81 | 82 | it('Бросает исключение, если передать не вектор в качестве расположения', () => { 83 | 84 | function fn() { 85 | const player = new Actor({ x: 12, y: 24 }); 86 | } 87 | 88 | expect(fn).to.throw(Error); 89 | }); 90 | 91 | it('Создает объект заданного размера, если передать вектор вторым аргументом', () => { 92 | const player = new Actor(undefined, size); 93 | 94 | expect(player.size).is.equal(size); 95 | }); 96 | 97 | it('Бросает исключение, если передать не вектор в качестве размера', () => { 98 | 99 | function fn() { 100 | const player = new Actor(undefined, { x: 12, y: 24 }); 101 | } 102 | 103 | expect(fn).to.throw(Error); 104 | }); 105 | 106 | it('Бросает исключение, если передать не вектор в качестве скорости', () => { 107 | 108 | function fn() { 109 | const player = new Actor(undefined, undefined, { x: 12, y: 24 }); 110 | } 111 | 112 | expect(fn).to.throw(Error); 113 | }); 114 | }); 115 | 116 | describe('Границы объекта', () => { 117 | it('Имеет свойство left, которое содержит координату левой границы объекта по оси X', () => { 118 | const player = new Actor(position, size); 119 | 120 | expect(player.left).is.equal(30); 121 | }); 122 | 123 | it('Имеет свойство right, которое содержит координату правой границы объекта по оси X', () => { 124 | const player = new Actor(position, size); 125 | 126 | expect(player.right).is.equal(35); 127 | }); 128 | 129 | it('Имеет свойство top, которое содержит координату верхней границы объекта по оси Y', () => { 130 | const player = new Actor(position, size); 131 | 132 | expect(player.top).is.equal(50); 133 | }); 134 | 135 | it('Имеет свойство bottom, которое содержит координату нижней границы объекта по оси Y', () => { 136 | const player = new Actor(position, size); 137 | 138 | expect(player.bottom).is.equal(55); 139 | }); 140 | }); 141 | 142 | describe('Метод isIntersect', () => { 143 | it('Если передать объект, не являющийся экземпляром Actor, то получим исключение', () => { 144 | const player = new Actor(); 145 | 146 | function fn() { 147 | player.isIntersect({ left: 0, top: 0, bottom: 1, right: 1 }); 148 | } 149 | 150 | expect(fn).to.throw(Error); 151 | }); 152 | 153 | it('Объект не пересекается сам с собой', () => { 154 | const player = new Actor(position, size); 155 | 156 | const notIntersected = player.isIntersect(player); 157 | 158 | expect(notIntersected).is.equal(false); 159 | }); 160 | 161 | it('Объект не пересекается с объектом, расположенным очень далеко', () => { 162 | const player = new Actor(new Vector(0, 0)); 163 | const coin = new Actor(new Vector(100, 100)); 164 | 165 | const notIntersected = player.isIntersect(coin); 166 | 167 | expect(notIntersected).is.equal(false); 168 | }); 169 | 170 | it('Объект не пересекается с объектом со смежными границами', () => { 171 | const player = new Actor(position, size); 172 | 173 | const moveX = new Vector(1, 0); 174 | const moveY = new Vector(0, 1); 175 | 176 | const coins = [ 177 | new Actor(position.plus(moveX.times(-1))), 178 | new Actor(position.plus(moveY.times(-1))), 179 | new Actor(position.plus(size).plus(moveX)), 180 | new Actor(position.plus(size).plus(moveY)) 181 | ]; 182 | 183 | coins.forEach(coin => { 184 | const notIntersected = player.isIntersect(coin); 185 | 186 | expect(notIntersected).is.equal(false); 187 | }); 188 | }); 189 | 190 | it('Объект не пересекается с объектом, расположенным в той же точке, но имеющим отрицательный вектор размера', () => { 191 | const player = new Actor(new Vector(0, 0), new Vector(1, 1)); 192 | const coin = new Actor(new Vector(0, 0), new Vector(1, 1).times(-1)); 193 | 194 | const notIntersected = player.isIntersect(coin); 195 | 196 | expect(notIntersected).is.equal(false); 197 | }); 198 | 199 | it('Объект пересекается с объектом, который полностью содержится в нём', () => { 200 | const player = new Actor(new Vector(0, 0), new Vector(100, 100)); 201 | const coin = new Actor(new Vector(10, 10), new Vector()); 202 | 203 | const intersected = player.isIntersect(coin); 204 | 205 | expect(intersected).is.equal(true); 206 | }); 207 | 208 | it('Объект пересекается с объектом, который частично содержится в нём', () => { 209 | const player = new Actor(position, size); 210 | 211 | const moveX = new Vector(1, 0); 212 | const moveY = new Vector(0, 1); 213 | 214 | const coins = [ 215 | new Actor(position.plus(moveX.times(-1)), size), 216 | new Actor(position.plus(moveY.times(-1)), size), 217 | new Actor(position.plus(moveX), size), 218 | new Actor(position.plus(moveY), size) 219 | ]; 220 | 221 | coins.forEach(coin => { 222 | const intersected = player.isIntersect(coin); 223 | 224 | expect(intersected).is.equal(true); 225 | }); 226 | }); 227 | 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /test/coin.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс Coin', () => { 4 | let position; 5 | 6 | beforeEach(() => { 7 | position = new Vector(5, 5); 8 | }); 9 | 10 | describe('Конструктор new Coin', () => { 11 | it('Создает экземпляр Actor', () => { 12 | const coin = new Coin(); 13 | 14 | expect(coin).to.be.an.instanceof(Actor); 15 | }); 16 | 17 | it('Имеет свойство type, равное coin', () => { 18 | const coin = new Coin(); 19 | 20 | expect(coin.type).to.equal('coin'); 21 | }); 22 | 23 | it('Имеет размер Vector(0.6, 0.6)', () => { 24 | const coin = new Coin(); 25 | 26 | expect(coin.size).to.eql(new Vector(0.6, 0.6)); 27 | }); 28 | 29 | it('Реальная позиция сдвинута на Vector(0.2, 0.1)', () => { 30 | const coin = new Coin(position); 31 | const realPosition = new Vector(5.2, 5.1); 32 | 33 | expect(coin.pos).to.eql(realPosition); 34 | }); 35 | 36 | it('Имеет свойство spring, равное случайному числу от 0 до 2π', () => { 37 | const coin = new Coin(); 38 | 39 | expect(coin.spring).to.be.within(0, 2 * Math.PI); 40 | }); 41 | 42 | it('Имеет свойство springSpeed, равное 8', () => { 43 | const coin = new Coin(); 44 | 45 | expect(coin.springSpeed).to.equal(8); 46 | }); 47 | 48 | it('Имеет свойство springDist, равное 0.07', () => { 49 | const coin = new Coin(); 50 | 51 | expect(coin.springDist).to.equal(0.07); 52 | }); 53 | }); 54 | 55 | describe('Метод updateSpring', () => { 56 | it('Увеличит свойство spring на springSpeed', () => { 57 | const coin = new Coin(); 58 | const initialSpring = coin.spring; 59 | 60 | coin.updateSpring(); 61 | 62 | expect(coin.spring).to.equal(initialSpring + 8); 63 | }); 64 | 65 | it('Если передать время, увеличит свойство spring на springSpeed, умноженное на время', () => { 66 | const time = 5; 67 | const coin = new Coin(); 68 | const initialSpring = coin.spring; 69 | 70 | coin.updateSpring(time); 71 | 72 | expect(coin.spring).to.equal(initialSpring + 40); 73 | }); 74 | }); 75 | 76 | describe('Метод getSpringVector', () => { 77 | it('Вернет вектор', () => { 78 | const coin = new Coin(); 79 | 80 | const vector = coin.getSpringVector(); 81 | 82 | expect(vector).to.be.an.instanceof(Vector); 83 | }); 84 | 85 | it('Координата x этого вектора равна нулю', () => { 86 | const coin = new Coin(); 87 | 88 | const vector = coin.getSpringVector(); 89 | 90 | expect(vector.x).to.equal(0); 91 | }); 92 | 93 | it('Координата y этого вектора равна синусу от spring, умноженному на springDist', () => { 94 | const coin = new Coin(); 95 | 96 | const vector = coin.getSpringVector(); 97 | 98 | expect(vector.y).to.equal(Math.sin(coin.spring) * 0.07); 99 | }); 100 | }); 101 | 102 | describe('Метод getNextPosition', () => { 103 | it('Увеличит spring на springSpeed', () => { 104 | const coin = new Coin(position); 105 | const initialSpring = coin.spring; 106 | 107 | coin.getNextPosition(); 108 | 109 | expect(coin.spring).to.equal(initialSpring + 8); 110 | }); 111 | 112 | it('Если передать время, увеличит свойство spring на springSpeed, умноженное на время', () => { 113 | const time = 5; 114 | const coin = new Coin(); 115 | const initialSpring = coin.spring; 116 | 117 | coin.getNextPosition(time); 118 | 119 | expect(coin.spring).to.equal(initialSpring + 40); 120 | }); 121 | 122 | it('Вернет вектор', () => { 123 | const coin = new Coin(position); 124 | 125 | const newPosition = coin.getNextPosition(); 126 | 127 | expect(newPosition).to.be.an.instanceof(Vector); 128 | }); 129 | 130 | it('Координата x новой позиции не изменится', () => { 131 | const coin = new Coin(position); 132 | const realPosition = coin.pos; 133 | 134 | const newPosition = coin.getNextPosition(); 135 | 136 | expect(newPosition.x).to.equal(realPosition.x); 137 | }); 138 | 139 | it('Координата y новой позиции будет в пределах исходного значения y и y + 1', () => { 140 | const coin = new Coin(position); 141 | 142 | const newPosition = coin.getNextPosition(); 143 | expect(newPosition.y).to.be.within(position.y, position.y + 1); 144 | }); 145 | 146 | it('Вернет новую позицию, увеличив старую на вектор подпрыгивания', () => { 147 | const coin = new Coin(position); 148 | const realPosition = coin.pos; 149 | 150 | const newPosition = coin.getNextPosition(); 151 | const springVector = coin.getSpringVector(); 152 | 153 | expect(newPosition).to.eql(realPosition.plus(springVector)); 154 | }); 155 | 156 | it('Увеличивается вектор исходной позиции, а не текущей', () => { 157 | const coin = new Coin(position); 158 | const realPosition = coin.pos; 159 | 160 | coin.pos = coin.getNextPosition(); 161 | 162 | const newPosition = coin.getNextPosition(); 163 | const springVector = coin.getSpringVector(); 164 | 165 | expect(newPosition).to.eql(realPosition.plus(springVector)); 166 | }); 167 | }); 168 | 169 | describe('Метод act', () => { 170 | it('Обновит текущую позицию на ту, что вернет getNextPosition', () => { 171 | const time = 5; 172 | const coin = new Coin(position); 173 | const spring = coin.spring; 174 | const newPosition = coin.getNextPosition(time); 175 | coin.spring = spring; 176 | 177 | coin.act(time); 178 | 179 | expect(coin.pos).to.eql(newPosition); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /test/fireball.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс Fireball', () => { 4 | let time, speed, position; 5 | 6 | beforeEach(() => { 7 | time = 5; 8 | speed = new Vector(1, 0); 9 | position = new Vector(5, 5); 10 | }); 11 | 12 | describe('Конструктор new Fireball', () => { 13 | it('Созданный объект является экземпляром Actor', () => { 14 | const ball = new Fireball(); 15 | 16 | expect(ball).to.be.an.instanceof(Actor); 17 | }); 18 | 19 | it('Имеет свойство type, равное fireball', () => { 20 | const ball = new Fireball(); 21 | 22 | expect(ball.type).to.equal('fireball'); 23 | }); 24 | 25 | it('Имеет свойство speed, равное вектору Vector, переданному вторым аргументом', () => { 26 | const ball = new Fireball(undefined, speed); 27 | 28 | expect(ball.speed).to.eql(speed); 29 | }); 30 | 31 | it('Свойство pos равно вектору Vector, переданному первым аргументом', () => { 32 | const ball = new Fireball(position); 33 | 34 | expect(ball.pos).to.equal(position); 35 | }); 36 | }); 37 | 38 | describe('Метод getNextPosition', () => { 39 | it('Вернет ту же позицию для объекта с нулевой скоростью', () => { 40 | const zeroSpeed = new Vector(0, 0); 41 | const ball = new Fireball(position, zeroSpeed); 42 | 43 | const nextPosition = ball.getNextPosition(); 44 | 45 | expect(nextPosition).to.eql(position); 46 | }); 47 | 48 | it('Вернет новую позицию, увеличенную на вектор скорости', () => { 49 | const ball = new Fireball(position, speed); 50 | 51 | const nextPosition = ball.getNextPosition(); 52 | 53 | expect(nextPosition).to.eql(new Vector(6, 5)); 54 | }); 55 | 56 | it('Если передать время первым аргументом, то вернет новую позицию, увеличенную на вектор скорости, помноженный на переданное время', () => { 57 | const ball = new Fireball(position, speed); 58 | 59 | const nextPosition = ball.getNextPosition(time); 60 | 61 | expect(nextPosition).to.eql(new Vector(10, 5)); 62 | }); 63 | }); 64 | 65 | describe('Метод handleObstacle', () => { 66 | it('Меняет вектор скорости на противоположный', () => { 67 | const ball = new Fireball(position, speed); 68 | 69 | ball.handleObstacle(); 70 | 71 | expect(ball.speed).to.eql(new Vector(-1, -0)); 72 | }); 73 | }); 74 | 75 | describe('Метод act', () => { 76 | it('Если препятствий нет, меняет позицию на ту, что получена с помощью getNextPosition', () => { 77 | const level = { 78 | obstacleAt() { 79 | return undefined; 80 | } 81 | }; 82 | const ball = new Fireball(position, speed); 83 | const nextPosition = new Vector(10, 5); 84 | 85 | ball.act(time, level); 86 | 87 | expect(ball.speed).to.eql(speed); 88 | expect(ball.pos).to.eql(nextPosition); 89 | }); 90 | 91 | it('При столкновении с препятствием не меняет позицию объекта, меняет вектор скорости на противоположный', () => { 92 | const level = { 93 | obstacleAt() { 94 | return 'wall'; 95 | } 96 | }; 97 | const ball = new Fireball(position, speed); 98 | 99 | ball.act(time, level); 100 | 101 | expect(ball.speed).to.eql(new Vector(-1, -0)); 102 | expect(ball.pos).to.eql(position); 103 | }); 104 | 105 | it('Вызывает level.obstacleAt со своим вектором размера', () => { 106 | const ball = new Fireball(position, speed); 107 | let isCalled = false; 108 | const level = { 109 | obstacleAt(pos, size) { 110 | expect(size).to.eql(new Vector(1, 1)); 111 | isCalled = true; 112 | } 113 | }; 114 | 115 | ball.act(time, level); 116 | expect(isCalled).to.be.true; 117 | }); 118 | 119 | it('Вызывает level.obstacleAt с вектором новой позиции', () => { 120 | const ball = new Fireball(position, speed); 121 | let isCalled = false; 122 | const level = { 123 | obstacleAt(pos, size) { 124 | expect(pos).to.eql(new Vector(10, 5)); 125 | isCalled = true; 126 | } 127 | }; 128 | 129 | ball.act(time, level); 130 | expect(isCalled).to.be.true; 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/firerain.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс FireRain', () => { 4 | let position; 5 | 6 | beforeEach(() => { 7 | position = new Vector(5, 5); 8 | }); 9 | 10 | describe('Конструктор new FireRain', () => { 11 | it('Создает экземпляр Fireball', () => { 12 | const ball = new FireRain(); 13 | 14 | expect(ball).to.be.an.instanceof(Fireball); 15 | }); 16 | 17 | it('Имеет скорость Vector(0, 3)', () => { 18 | const ball = new FireRain(); 19 | 20 | expect(ball.speed).to.eql(new Vector(0, 3)); 21 | }); 22 | 23 | it('Имеет свойство type, равное fireball', () => { 24 | const ball = new HorizontalFireball(); 25 | 26 | expect(ball.type).to.equal('fireball'); 27 | }); 28 | }); 29 | 30 | describe('Метод handleObstacle', () => { 31 | it('Не меняет вектор скорости', () => { 32 | const ball = new FireRain(position); 33 | 34 | ball.handleObstacle(); 35 | 36 | expect(ball.speed).to.eql(new Vector(0, 3)); 37 | }); 38 | 39 | it('Меняет позицию на исходную', () => { 40 | const ball = new FireRain(position); 41 | ball.pos = new Vector(100, 100); 42 | 43 | ball.handleObstacle(); 44 | 45 | expect(ball.pos).to.eql(position); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/horizontalfireball.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс HorizontalFireball', () => { 4 | describe('Конструктор new HorizontalFireball', () => { 5 | it('Создает экземпляр Fireball', () => { 6 | const ball = new HorizontalFireball(); 7 | 8 | expect(ball).to.be.an.instanceof(Fireball); 9 | }); 10 | 11 | it('Имеет скорость Vector(2, 0)', () => { 12 | const ball = new HorizontalFireball(); 13 | 14 | expect(ball.speed).to.eql(new Vector(2, 0)); 15 | }); 16 | 17 | it('Имеет свойство type, равное fireball', () => { 18 | const ball = new HorizontalFireball(); 19 | 20 | expect(ball.type).to.equal('fireball'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/level.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс Level', () => { 4 | const Player = extend(Actor, { type: { value: 'player' }}); 5 | const Mushroom = extend(Actor, { type: { value: 'mushroom' }}); 6 | const Gift = extend(Actor, { type: { value: 'gift' }}); 7 | const Coin = extend(Actor, { type: { value: 'coin' }}); 8 | 9 | let player, mushroom, giftBig, giftSmall, goldCoin, bronzeCoin; 10 | 11 | beforeEach(() => { 12 | player = new Player; 13 | mushroom = new Mushroom; 14 | giftBig = new Gift; 15 | giftSmall = new Gift; 16 | goldCoin = new Coin; 17 | bronzeCoin = new Coin; 18 | 19 | player.title = 'Игрок'; 20 | mushroom.title = 'Гриб'; 21 | giftBig.title = 'Большой сюрприз'; 22 | giftSmall.title = 'Маленький сюрприз'; 23 | goldCoin.title = 'Золотая монета'; 24 | bronzeCoin.title = 'Бронзовая монета'; 25 | }); 26 | 27 | describe('Конструктор new Level', () => { 28 | it('Высота пустого уровня равна 0', () => { 29 | const level = new Level(); 30 | 31 | expect(level.height).to.equal(0); 32 | }); 33 | 34 | it('Ширина пустого уровня равна 0', () => { 35 | const level = new Level(); 36 | 37 | expect(level.width).to.equal(0); 38 | }); 39 | 40 | it('Высота уровня равна количеству строк сетки', () => { 41 | const lines = 100; 42 | const grid = new Array(lines); 43 | 44 | const level = new Level(grid); 45 | 46 | expect(level.height).to.equal(lines); 47 | }); 48 | 49 | it('Ширина уровня равна количеству ячеек сетки', () => { 50 | const lines = 100; 51 | const cells = 50; 52 | const grid = new Array(lines).fill(new Array(cells)); 53 | 54 | const level = new Level(grid); 55 | 56 | expect(level.width).to.equal(cells); 57 | }); 58 | 59 | it('Если в строках разное количество ячеек, то ширина уровня равна количеству ячеек в самой длинной строке', () => { 60 | const lines = 100; 61 | const cells = 50; 62 | const maxCells = 100; 63 | const grid = new Array(lines).fill(new Array(cells)); 64 | grid[73].length = maxCells; 65 | 66 | const level = new Level(grid); 67 | 68 | expect(level.width).to.equal(maxCells); 69 | }); 70 | 71 | it('Имеет свойство status, равное null', () => { 72 | const level = new Level(); 73 | 74 | expect(level.status).to.be.null; 75 | }); 76 | 77 | it('Имеет свойство finishDelay, равное 1', () => { 78 | const level = new Level(); 79 | 80 | expect(level.finishDelay).to.equal(1); 81 | }); 82 | 83 | it('Имеет свойство actors, в котором все движущиеся объекты, переданные в конструктор', () => { 84 | const actors = [ player ]; 85 | const level = new Level(undefined, actors); 86 | 87 | expect(level.actors).to.eql(actors); 88 | }); 89 | 90 | it('Имеет свойство player, в котором движущийся объект со свойством type, равным player', () => { 91 | const level = new Level(undefined, [ player, mushroom ]); 92 | 93 | expect(level.player).to.equal(player); 94 | }); 95 | }); 96 | 97 | describe('Метод isFinished', () => { 98 | it('По умолчанию вернет false', () => { 99 | const level = new Level(); 100 | 101 | const isNotFinished = level.isFinished(); 102 | 103 | expect(isNotFinished).to.be.false; 104 | }); 105 | 106 | it('Вернет true, если status будет не равен null, и finishDelay меньше нуля', () => { 107 | const level = new Level(); 108 | 109 | level.status = 'lost'; 110 | level.finishDelay = -1; 111 | const isFinished = level.isFinished(); 112 | 113 | expect(isFinished).to.be.true; 114 | }); 115 | 116 | it('Вернет false, если status будет не равен null, но finishDelay будет больше нуля', () => { 117 | const level = new Level(); 118 | 119 | level.status = 'lost'; 120 | const isNotFinished = level.isFinished(); 121 | 122 | expect(isNotFinished).to.be.false; 123 | }); 124 | }); 125 | 126 | describe('Метод actorAt', () => { 127 | 128 | it('Выбросит исключение, если передать не движущийся объект Actor', () => { 129 | const level = new Level(undefined, [ player ]); 130 | 131 | function fn() { 132 | level.actorAt({}); 133 | } 134 | 135 | expect(fn).to.throw(Error); 136 | }); 137 | 138 | it('Вернет undefined для пустого уровня', () => { 139 | const level = new Level(); 140 | 141 | const noActor = level.actorAt(player); 142 | 143 | expect(noActor).to.be.undefined; 144 | }); 145 | 146 | it('Вернет undefined для уровня, в котором только один движущийся объект', () => { 147 | const level = new Level(undefined, [ player ]); 148 | 149 | const noActor = level.actorAt(player); 150 | 151 | expect(noActor).to.be.undefined; 152 | }); 153 | 154 | it('Вернет undefined, если ни один объект игрового поля не пересекается с переданным объектом', () => { 155 | const player = new Player(new Vector(1, 1)); 156 | const level = new Level(undefined, [ player, mushroom ]); 157 | 158 | const actor = level.actorAt(player); 159 | 160 | expect(actor).to.be.undefined; 161 | }); 162 | 163 | it('Вернет объект игрового поля, который пересекается с переданным объектом', () => { 164 | const level = new Level(undefined, [ player, mushroom ]); 165 | 166 | const actor = level.actorAt(player); 167 | 168 | expect(actor).to.be.equal(mushroom); 169 | }); 170 | 171 | }); 172 | 173 | describe('Метод obstacleAt', () => { 174 | const gridSize = 2; 175 | let grid, wallGrid, lavaGrid, size; 176 | 177 | beforeEach(() => { 178 | grid = new Array(gridSize).fill(new Array(gridSize)); 179 | wallGrid = new Array(gridSize).fill(new Array(gridSize).fill('wall')); 180 | lavaGrid = new Array(gridSize).fill(new Array(gridSize).fill('lava')); 181 | size = new Vector(1, 1); 182 | }); 183 | 184 | it('Вернет undefined, если объект не выходит за пределы уровня и ни с чем не пересекается', () => { 185 | const level = new Level(grid); 186 | const position = new Vector(0, 0); 187 | 188 | const wall = level.obstacleAt(position, size); 189 | 190 | expect(wall).to.be.undefined; 191 | }); 192 | 193 | it('Вернет строку wall, если левая граница объекта выходит за пределы уровня', () => { 194 | const level = new Level(grid); 195 | const position = new Vector(-1, 0); 196 | 197 | const wall = level.obstacleAt(position, size); 198 | 199 | expect(wall).to.be.equal('wall'); 200 | }); 201 | 202 | it('Вернет строку wall, если правая граница объекта выходит за пределы уровня', () => { 203 | const level = new Level(grid); 204 | const position = new Vector(gridSize, 0); 205 | 206 | const wall = level.obstacleAt(position, size); 207 | 208 | expect(wall).to.be.equal('wall'); 209 | }); 210 | 211 | it('Вернет строку wall, если верхняя граница объекта выходит за пределы уровня', () => { 212 | const level = new Level(grid); 213 | const position = new Vector(0, -1); 214 | 215 | const wall = level.obstacleAt(position, size); 216 | 217 | expect(wall).to.be.equal('wall'); 218 | }); 219 | 220 | it('Вернет строку lava, если нижняя граница объекта выходит за пределы уровня', () => { 221 | const level = new Level(grid); 222 | const position = new Vector(0, gridSize); 223 | 224 | const wall = level.obstacleAt(position, size); 225 | 226 | expect(wall).to.be.equal('lava'); 227 | }); 228 | 229 | it('Вернет строку wall, если площадь пересекается со стеной', () => { 230 | const level = new Level(wallGrid); 231 | const position = new Vector(0, 0); 232 | 233 | const wall = level.obstacleAt(position, size); 234 | 235 | expect(wall).to.be.equal('wall'); 236 | }); 237 | 238 | it('Вернет строку lava, если площадь пересекается с лавой', () => { 239 | const level = new Level(lavaGrid); 240 | const position = new Vector(0, 0); 241 | 242 | const wall = level.obstacleAt(position, size); 243 | 244 | expect(wall).to.be.equal('lava'); 245 | }); 246 | 247 | it('Вернет строку wall, если площадь пересекается со стеной и объект имеет нецелочисленные координаты', () => { 248 | const level = new Level(wallGrid); 249 | const position = new Vector(0.5, 0.5); 250 | 251 | const wall = level.obstacleAt(position, size); 252 | 253 | expect(wall).to.be.equal('wall'); 254 | }); 255 | 256 | it('Вернет строку wall, если площадь пересекается со стеной и объект имеет нецелочисленный размер', () => { 257 | const level = new Level(wallGrid); 258 | const position = new Vector(0, 0); 259 | const size = new Vector(0.5, 0.5); 260 | 261 | const wall = level.obstacleAt(position, size); 262 | 263 | expect(wall).to.be.equal('wall'); 264 | }); 265 | 266 | it('Объект не пересекается со стеной, на которой стоит', () => { 267 | const grid = [ 268 | Array(4), 269 | Array(4), 270 | Array(4), 271 | Array(4).fill('wall') 272 | ]; 273 | const level = new Level(grid); 274 | const position = new Vector(2.1, 1.5); 275 | const size = new Vector(0.8, 1.5); 276 | 277 | const nothing = level.obstacleAt(position, size); 278 | 279 | expect(nothing).to.be.undefined; 280 | }); 281 | 282 | it('Объект не пересекается со стеной, которая над ним', () => { 283 | const grid = [ 284 | Array(4).fill('wall'), 285 | Array(4), 286 | Array(4), 287 | Array(4) 288 | ]; 289 | const level = new Level(grid); 290 | const position = new Vector(2.1, 1); 291 | const size = new Vector(0.8, 1.5); 292 | 293 | const nothing = level.obstacleAt(position, size); 294 | 295 | expect(nothing).to.be.undefined; 296 | }); 297 | 298 | it('Объект не пересекается со стеной, которая слева от него', () => { 299 | const grid = [ 300 | Array(4), 301 | ['wall', undefined, undefined, undefined], 302 | Array(4), 303 | Array(4) 304 | ]; 305 | const level = new Level(grid); 306 | const position = new Vector(1, 1.5); 307 | const size = new Vector(0.8, 1.5); 308 | 309 | const nothing = level.obstacleAt(position, size); 310 | 311 | expect(nothing).to.be.undefined; 312 | }); 313 | 314 | it('Объект не пересекается со стеной, которая справа от него', () => { 315 | const grid = [ 316 | Array(4), 317 | [undefined, undefined, 'wall', undefined], 318 | Array(4), 319 | Array(4) 320 | ]; 321 | const level = new Level(grid); 322 | const position = new Vector(1.2, 1.5); 323 | const size = new Vector(0.8, 1.5); 324 | 325 | const nothing = level.obstacleAt(position, size); 326 | 327 | expect(nothing).to.be.undefined; 328 | }); 329 | }); 330 | 331 | describe('Метод removeActor', () => { 332 | it('Удаляет переданный движущийся объект', () => { 333 | const level = new Level(undefined, [ mushroom, giftSmall ]); 334 | 335 | level.removeActor(mushroom); 336 | 337 | expect(level.actors.includes(mushroom)).to.be.false; 338 | }); 339 | 340 | it('Не удаляет остальные движущиеся объекты', () => { 341 | const level = new Level(undefined, [ mushroom, giftSmall ]); 342 | 343 | level.removeActor(mushroom); 344 | 345 | expect(level.actors.includes(giftSmall)).to.be.true; 346 | }); 347 | }); 348 | 349 | describe('Метод noMoreActors', () => { 350 | it('Вернет истину, если движущихся объектов нет в уровне', () => { 351 | const level = new Level(); 352 | 353 | expect(level.noMoreActors()).to.be.true; 354 | }); 355 | 356 | it('Вернет истину, если в уровне нет движущихся объектов заданного типа', () => { 357 | const level = new Level(undefined, [ mushroom, giftSmall ]); 358 | 359 | expect(level.noMoreActors('actor')).to.be.true; 360 | }); 361 | 362 | it('Вернет ложь, если в уровне есть движущиеся объекты заданного типа', () => { 363 | const level = new Level(undefined, [ mushroom, giftSmall ]); 364 | 365 | expect(level.noMoreActors('mushroom')).to.be.false; 366 | }); 367 | }); 368 | 369 | describe('Метод playerTouched', () => { 370 | 371 | it('Если передать lava первым аргументом, меняет статус уровня на lost', () => { 372 | const level = new Level(); 373 | 374 | level.playerTouched('lava'); 375 | 376 | expect(level.status).to.equal('lost'); 377 | }); 378 | 379 | it('Если передать fireball первым аргументом, меняет статус уровня на lost', () => { 380 | const level = new Level(); 381 | 382 | level.playerTouched('fireball'); 383 | 384 | expect(level.status).to.equal('lost'); 385 | }); 386 | 387 | it('Если передать coin первым аргументом и движущийся объект вторым, удаляет этот объект из уровня', () => { 388 | const level = new Level(undefined, [ goldCoin, bronzeCoin ]); 389 | 390 | level.playerTouched('coin', goldCoin); 391 | 392 | expect(level.actors).to.have.length(1); 393 | expect(level.actors).to.not.include(goldCoin); 394 | expect(level.status).to.equal(null); 395 | }); 396 | 397 | it('Если удалить все монеты, то статус меняется на won', () => { 398 | const level = new Level(undefined, [ goldCoin, bronzeCoin ]); 399 | 400 | level.playerTouched('coin', goldCoin); 401 | level.playerTouched('coin', bronzeCoin); 402 | 403 | expect(level.status).to.equal('won'); 404 | }); 405 | 406 | it('Если удалить все монеты после касания lava, статус уровня остается lost', () => { 407 | const level = new Level(undefined, [ goldCoin, bronzeCoin ]); 408 | 409 | level.playerTouched('lava'); 410 | level.playerTouched('coin', goldCoin); 411 | level.playerTouched('coin', bronzeCoin); 412 | 413 | expect(level.status).to.equal('lost'); 414 | }); 415 | 416 | it('Если удалить все монеты после касания fireball, статус уровня остается lost', () => { 417 | const level = new Level(undefined, [ goldCoin, bronzeCoin ]); 418 | 419 | level.playerTouched('fireball'); 420 | level.playerTouched('coin', goldCoin); 421 | level.playerTouched('coin', bronzeCoin); 422 | 423 | expect(level.status).to.equal('lost'); 424 | }); 425 | 426 | it('Если коснуться lava после удаления всех монет, статус уровня остается won', () => { 427 | const level = new Level(undefined, [ goldCoin, bronzeCoin ]); 428 | 429 | level.playerTouched('coin', goldCoin); 430 | level.playerTouched('coin', bronzeCoin); 431 | level.playerTouched('lava'); 432 | 433 | expect(level.status).to.equal('won'); 434 | }); 435 | 436 | it('Если коснуться fireball после удаления всех монет, статус уровня остается won', () => { 437 | const level = new Level(undefined, [ goldCoin, bronzeCoin ]); 438 | 439 | level.playerTouched('coin', goldCoin); 440 | level.playerTouched('coin', bronzeCoin); 441 | level.playerTouched('fireball'); 442 | 443 | expect(level.status).to.equal('won'); 444 | }); 445 | }); 446 | }); 447 | -------------------------------------------------------------------------------- /test/parser.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс LevelParser', () => { 4 | const Mushroom = extend(Actor, { type: { value: 'mushroom' }}); 5 | const Gift = extend(Actor, { type: { value: 'gift' }}); 6 | class BadActor {} 7 | 8 | describe('Конструктор new LevelParser()', () => { 9 | }); 10 | 11 | describe('Метод actorFromSymbol', () => { 12 | 13 | 14 | it('Вернет undefined, если не передать символ', () => { 15 | const parser = new LevelParser(); 16 | 17 | const actor = parser.actorFromSymbol(); 18 | 19 | expect(actor).to.be.undefined; 20 | }); 21 | 22 | it('Вернет undefined, если передать символ, которому не назначен конструктор движимого объекта', () => { 23 | const parser = new LevelParser({ y: Mushroom }); 24 | 25 | const actor = parser.actorFromSymbol('z'); 26 | 27 | expect(actor).to.be.undefined; 28 | }); 29 | 30 | it('Вернет подходящий конструктор движимого объекта, если передать символ, которому он назначен', () => { 31 | const parser = new LevelParser({ y: Mushroom }); 32 | 33 | const actor = parser.actorFromSymbol('y'); 34 | 35 | expect(actor).to.equal(Mushroom); 36 | }); 37 | }); 38 | 39 | describe('Метод obstacleFromSymbol', () => { 40 | it('Вернет undefined, если не передать символ', () => { 41 | const parser = new LevelParser(); 42 | 43 | const obstacle = parser.obstacleFromSymbol(); 44 | 45 | expect(obstacle).to.be.undefined; 46 | }); 47 | 48 | it('Вернет undefined, если передать неизвестный символ', () => { 49 | const parser = new LevelParser(); 50 | 51 | const obstacle = parser.obstacleFromSymbol('Z'); 52 | 53 | expect(obstacle).to.be.undefined; 54 | }); 55 | 56 | it('Вернет wall, если передать символ x', () => { 57 | const parser = new LevelParser(); 58 | 59 | const obstacle = parser.obstacleFromSymbol('x'); 60 | 61 | expect(obstacle).to.equal('wall'); 62 | }); 63 | 64 | it('Вернет lava, если передать символ !', () => { 65 | const parser = new LevelParser(); 66 | 67 | const obstacle = parser.obstacleFromSymbol('!'); 68 | 69 | expect(obstacle).to.equal('lava'); 70 | }); 71 | }); 72 | 73 | describe('Метод createGrid', () => { 74 | let plan; 75 | 76 | beforeEach(() => { 77 | plan = [ 78 | 'x x', 79 | '!!!!' 80 | ]; 81 | }); 82 | 83 | it('Вернет пустой массив, если передать пустой план', () => { 84 | const parser = new LevelParser(); 85 | 86 | const grid = parser.createGrid([]); 87 | 88 | expect(grid).to.eql([]); 89 | }); 90 | 91 | it('Вернет массив того же размера, что и plan', () => { 92 | const parser = new LevelParser(); 93 | 94 | const grid = parser.createGrid(plan); 95 | 96 | expect(grid.length).to.equal(plan.length); 97 | }); 98 | 99 | it('В ряду будет столько элементов, сколько символов в строке плана', () => { 100 | const parser = new LevelParser(); 101 | 102 | const grid = parser.createGrid(plan); 103 | 104 | grid.forEach((row, y) => { 105 | expect(row.length).to.equal(plan[y].length); 106 | }); 107 | }); 108 | 109 | it('Символы x определит как wall и поместит в соответствующую ячейку', () => { 110 | const parser = new LevelParser(); 111 | 112 | const grid = parser.createGrid(plan); 113 | 114 | expect(grid[0]).to.eql(['wall',undefined,undefined,'wall']); 115 | }); 116 | 117 | it('Символы ! определит как lava и поместит в соответствующую ячейку', () => { 118 | const parser = new LevelParser(); 119 | 120 | const grid = parser.createGrid(plan); 121 | 122 | expect(grid[1]).to.eql(new Array(4).fill('lava')); 123 | }); 124 | }); 125 | 126 | describe('Метод createActors', () => { 127 | let plan; 128 | 129 | beforeEach(() => { 130 | plan = [ 131 | 'o o', 132 | ' z ', 133 | 'o o' 134 | ]; 135 | }); 136 | 137 | it('Вернет пустой массив, если передать пустой план', () => { 138 | const parser = new LevelParser({ o: Gift, z: Mushroom }); 139 | 140 | const actors = parser.createActors([]); 141 | 142 | expect(actors).to.eql([]); 143 | }); 144 | 145 | it('Вернет пустой массив, если не определить символы движущихся объектов', () => { 146 | const parser = new LevelParser(); 147 | 148 | const actors = parser.createActors(plan); 149 | 150 | expect(actors).to.eql([]); 151 | }); 152 | 153 | it('Игнорирует символы, для которых в словаре не задан символ', () => { 154 | const parser = new LevelParser({ z: 'mushroom' }); 155 | 156 | const actors = parser.createActors(['m']); 157 | 158 | expect(actors).to.eql([]); 159 | }); 160 | 161 | it('Игнорирует символы, для которых в словаре передана не функция', () => { 162 | const parser = new LevelParser({ z: 'mushroom' }); 163 | 164 | const actors = parser.createActors(['z']); 165 | 166 | expect(actors).to.eql([]); 167 | }); 168 | 169 | it('Игнорирует символы, для которых в словаре передан конструктор не Actor', () => { 170 | const parser = new LevelParser({ b: BadActor }); 171 | 172 | const actors = parser.createActors(['b']); 173 | 174 | expect(actors).to.eql([]); 175 | }); 176 | 177 | it('Создает движущиеся объекты для конструктора Actor', () => { 178 | const parser = new LevelParser({ z: Actor }); 179 | 180 | const actors = parser.createActors(['z']); 181 | 182 | expect(actors).to.have.length(1); 183 | }); 184 | 185 | it('Создает движущиеся объекты правильного типа для конструктора Actor ', () => { 186 | const parser = new LevelParser({ z: Actor }); 187 | 188 | const actors = parser.createActors(['z']); 189 | 190 | expect(actors[0]).to.be.an.instanceof(Actor); 191 | }); 192 | 193 | it('Создает движущиеся объекты для конструкторов типа Actor', () => { 194 | const parser = new LevelParser({ z: Mushroom }); 195 | 196 | const actors = parser.createActors(['z']); 197 | 198 | expect(actors).to.have.length(1); 199 | }); 200 | 201 | it('Создает движущиеся объекты правильного типа для конструкторов типа Actor ', () => { 202 | const parser = new LevelParser({ z: Mushroom }); 203 | 204 | const actors = parser.createActors(['z']); 205 | 206 | expect(actors[0]).to.be.an.instanceof(Mushroom); 207 | }); 208 | 209 | it('Вернет массив со всеми движущимися объектами, если передать план', () => { 210 | const parser = new LevelParser({ o: Gift, z: Mushroom }); 211 | 212 | const actors = parser.createActors(plan); 213 | 214 | expect(actors).to.have.length(5); 215 | }); 216 | 217 | it('Каждый движущийся объект будет экземпляром своего класса', () => { 218 | const parser = new LevelParser({ o: Gift, z: Mushroom }); 219 | 220 | const actors = parser.createActors(plan); 221 | const oActors = actors.filter(actor => actor instanceof Gift); 222 | const zActors = actors.filter(actor => actor instanceof Mushroom); 223 | 224 | expect(oActors).to.have.length(4); 225 | expect(zActors).to.have.length(1); 226 | }); 227 | 228 | it('Каждый движущийся объект будет иметь координаты той ячейки, где он размещен на плане', () => { 229 | const parser = new LevelParser({ o: Actor, z: Actor }); 230 | 231 | const actors = parser.createActors(plan); 232 | 233 | expect(actors.some(actor => actor.pos.x === 0 && actor.pos.y === 0)).to.be.true; 234 | expect(actors.some(actor => actor.pos.x === 4 && actor.pos.y === 0)).to.be.true; 235 | expect(actors.some(actor => actor.pos.x === 2 && actor.pos.y === 1)).to.be.true; 236 | expect(actors.some(actor => actor.pos.x === 0 && actor.pos.y === 0)).to.be.true; 237 | expect(actors.some(actor => actor.pos.x === 4 && actor.pos.y === 2)).to.be.true; 238 | }); 239 | }); 240 | 241 | describe('Метод parse', () => { 242 | let plan; 243 | 244 | beforeEach(() => { 245 | plan = [ 246 | ' oxo ', 247 | '!xzx!', 248 | ' oxo ' 249 | ]; 250 | }); 251 | 252 | it('Вернет объект уровня, Level', () => { 253 | const parser = new LevelParser(); 254 | 255 | const level = parser.parse([]); 256 | 257 | expect(level).to.be.an.instanceof(Level); 258 | }); 259 | 260 | it('Высота уровня будет равна количеству строк плана', () => { 261 | const parser = new LevelParser(); 262 | 263 | const level = parser.parse(plan); 264 | 265 | expect(level.height).to.equal(3); 266 | }); 267 | 268 | it('Ширина уровня будет равна количеству символов в максимальной строке плана', () => { 269 | const parser = new LevelParser(); 270 | 271 | const level = parser.parse(plan); 272 | 273 | expect(level.width).to.equal(5); 274 | }); 275 | 276 | it('Создаст уровень с движущимися объектами из плана', () => { 277 | const parser = new LevelParser({ o: Gift, z: Mushroom }); 278 | 279 | const level = parser.parse(plan); 280 | 281 | expect(level.actors).to.have.length(5); 282 | }); 283 | 284 | it('Создаст уровень с препятствиями из плана', () => { 285 | const parser = new LevelParser(); 286 | 287 | const level = parser.parse(plan); 288 | 289 | expect(level.grid).to.eql([ 290 | [undefined, undefined,'wall',undefined,undefined], 291 | ['lava','wall',undefined,'wall','lava'], 292 | [undefined,undefined,'wall',undefined,undefined] 293 | ]); 294 | }); 295 | }); 296 | }); 297 | -------------------------------------------------------------------------------- /test/player.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс Player', () => { 4 | let position; 5 | 6 | beforeEach(() => { 7 | position = new Vector(10, 5); 8 | }); 9 | 10 | describe('Конструктор', () => { 11 | it('Создает объект, реальная позиция которого отличается от той, что передана в конструктор, на вектор 0:-0,5', () => { 12 | const player = new Player(position); 13 | 14 | expect(player.pos).to.eql(new Vector(10, 4.5)); 15 | }); 16 | 17 | it('Создает объект размером 0,8:1,5', () => { 18 | const player = new Player(); 19 | 20 | expect(player.size).to.eql(new Vector(0.8, 1.5)); 21 | }); 22 | 23 | it('Имеет свойство type, равное player', () => { 24 | const player = new Player(); 25 | 26 | expect(player.type).to.equal('player'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/vector.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс Vector', () => { 4 | const x = 3, y = 7, left = 5, top = 10, n = 5; 5 | 6 | describe('Конструктор new Vector()', () => { 7 | it('создает объект со свойствами x и y, равными аргументам конструктора', () => { 8 | const position = new Vector(left, top); 9 | 10 | expect(position.x).is.equal(left); 11 | expect(position.y).is.equal(top); 12 | }); 13 | 14 | it('без аргументов создает объект со свойствами x и y, равными 0', () => { 15 | const position = new Vector(); 16 | 17 | expect(position.x).is.equal(0); 18 | expect(position.y).is.equal(0); 19 | }); 20 | 21 | }); 22 | 23 | describe('Метод plus()', () => { 24 | it('бросает исключение, если передать не вектор', () => { 25 | const position = new Vector(x, y); 26 | 27 | function fn() { 28 | position.plus({ left, top }); 29 | } 30 | 31 | expect(fn).to.throw(Error); 32 | }); 33 | 34 | it('создает новый вектор', () => { 35 | const position = new Vector(x, y); 36 | 37 | const newPosition = position.plus(new Vector(left, top)); 38 | 39 | expect(newPosition).is.instanceof(Vector); 40 | }); 41 | 42 | it('координаты нового вектора равны сумме координат суммируемых векторов', () => { 43 | const position = new Vector(x, y); 44 | 45 | const newPosition = position.plus(new Vector(left, top)); 46 | 47 | expect(newPosition.x).is.equal(8); 48 | expect(newPosition.y).is.equal(17); 49 | }); 50 | }); 51 | 52 | describe('Метод times()', () => { 53 | it('создает новый вектор', () => { 54 | const position = new Vector(x, y); 55 | 56 | const newPosition = position.times(n); 57 | 58 | expect(newPosition).is.instanceof(Vector); 59 | }); 60 | 61 | it('координаты нового вектора увеличены в n раз', () => { 62 | const position = new Vector(x, y); 63 | 64 | const newPosition = position.times(n); 65 | 66 | expect(newPosition.x).is.equal(15); 67 | expect(newPosition.y).is.equal(35); 68 | }); 69 | }); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /test/verticalfireball.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Класс VerticalFireball', () => { 4 | describe('Конструктор new VerticalFireball', () => { 5 | it('Создает экземпляр Fireball', () => { 6 | const ball = new VerticalFireball(); 7 | 8 | expect(ball).to.be.an.instanceof(Fireball); 9 | }); 10 | 11 | it('Имеет скорость Vector(0, 2)', () => { 12 | const ball = new VerticalFireball(); 13 | 14 | expect(ball.speed).to.eql(new Vector(0, 2)); 15 | }); 16 | 17 | it('Имеет свойство type, равное fireball', () => { 18 | const ball = new HorizontalFireball(); 19 | 20 | expect(ball.type).to.equal('fireball'); 21 | }); 22 | }); 23 | }); 24 | --------------------------------------------------------------------------------