Недели 7-8 · Not Fight Club

🧭 ← Podcast · Следующая → HTML Builder ·

🎯 Что строим

Turn-based браузерная игра в духе Бойцовского клуба — пять экранов, бой по раундам с зонами атаки и защиты, опонент-пул, critical hits, battle log, и полная персистентность в localStorage.

Это самый большой vanilla JS проект bootcamp до Async Race. Тут уже не «фичи на странице» — тут архитектура: state, render, event handling, переходы между экранами, сохранение прогресса. Если влепить всё в один main.js спагетти-стилем — задохнёшься на 5-й фиче.

5 экранов:

  1. Registration — ввод имени игрока
  2. Home — кнопка «Start battle»
  3. Character — аватар, имя, win/loss, смена аватара
  4. Settings — смена имени
  5. Battle — основной геймплей с HP-барами и battle log

Battle mechanics:

  • Зоны атаки (1 за ход) и защиты (2 за ход): head / body / legs
  • ≥ 2 опонента с разными профилями (например spider бьёт 2 зоны и блокирует 1, troll бьёт 1 и блокирует 3)
  • Damage наносится только там, где attack-зона не совпадает с defense-зоной
  • Critical hits — шанс на ×1.5 damage и пробитие блока
  • HP должно быть ≥ 3× damage (иначе бой кончится за 1 ход → −35)
  • Battle log: WHO атаковал, WHOM, WHERE, HOW MUCH damage

Persistence: имя, аватар, win/loss + бонус +30 за resume in-progress battle (HP, ход, log).

📄 Полное задание на GitHub →

🏷 Required Skills (как заявлено в задании)

vanilla JavaScript · DOM manipulation · state management · event handling · localStorage · game logic · randomization · UI components

🚫 Запреты (нарушишь — штрафы)

Что нельзя Штраф
🔴 UI-фреймворк (React/Vue/Angular/Svelte/Solid) −300 (весь проект в 0)
🔴 HP < 3× базового damage (бой кончается за 1 ход) −35
🟡 Console errors при нормальном использовании −15
🟡 TypeScript не запрещён, но в скоринге считается за vanilla JS только если читается
🟡 Любая сторонняя стейт-либа (Redux, MobX, Zustand) по духу запрета на фреймворки — не тащи

💡 Разрешено: любые нативные браузерные API, CSS-фреймворки и иконки (по желанию), client-side routing (но не обязателен — в задании сказано прямо).


⛰ Что унаследовано из P3 + Podcast (compound)

Из Shelter P3 ты уже умеешь:

  • DOM-манипуляции: querySelector, createElement, append, textContent
  • Event handling + делегирование
  • Modal/popup паттерн (battle screen — это по сути большой modal с state)
  • JSON как источник данных (тут будет opponents-pool в виде JS-объекта или JSON)

Из Podcast:

  • Работа с массивами объектов (map/filter/find)
  • Простой state через объект-синглтон + ре-рендер
  • Архитектура файлов: разделение на data / view / logic

На что эта задача его выводит:

  • Из «вью на 1 страницу» → в 5 экранов с переключением
  • Из «state в переменной» → в persistent state с восстановлением после reload
  • Из «обработчик клика» → в game loop с очередностью ходов

📚 Что изучить (по порядку)

⚠️ Идти по порядку. Сначала ООП и state, потом игровая логика, потом персистентность. Если кинешься писать battle.js до того как разберёшься как хранить персонажа — переделаешь 3 раза.

📥 Что должен знать ДО старта

После Shelter P3 и Podcast у тебя должно быть:

Если что-то из этого «плыву» — вернись и закрой, иначе на этом проекте утонешь.


1 · JavaScript объекты и работа с ними ⭐

Зачем: игрок, опонент, профиль атаки, лог-запись — всё это объекты. Будешь читать их свойства, мутировать (или копировать — спорный вопрос), сериализовать в JSON.

Self-check: объясни разницу Object.assign({}, obj) и structuredClone(obj). Что произойдёт если ты сохранишь в localStorage объект с вложенным массивом и потом изменишь оригинал?


2 · Классы и конструкторы ⭐

Зачем: Player, Opponent, Battle, Logger — это классы. Можно и через factory-функции (см. блок 4), но классы дают чище API: player.takeDamage(20), battle.startTurn.

Self-check: напиши класс Fighter с приватным #hp, конструктором (name, hp, damage) и методом takeDamage(amount). Почему this ломается если передаёшь метод как callback setTimeout(fighter.tick, 1000)?


3 · State management в vanilla JS ⭐⭐⭐

Зачем: это ядро задачи. Где лежит currentScreen? Как Settings → Character узнают что имя поменялось? Как battle помнит чей сейчас ход после reload?

📝 Идея направления (не код):

«State — это один большой объект-источник правды. UI — это функция от state: render(state). Любое действие меняет state → вызывает re-render. Никаких document.querySelector('.name').textContent = ... разбросанных по 10 функциям.»

Self-check: нарисуй схему: «player нажал кнопку attack → ... → HP-бар обновился». Сколько шагов? Где state живёт? Кто его меняет? Кто узнаёт об изменении?


4 · ООП: композиция vs наследование ⭐

Зачем: велик соблазн сделать class Player extends Fighter и class Opponent extends Fighter. Это работает, но через ~3 фичи упрёшься в diamond problem. Композиция чаще выигрывает.

Self-check: у тебя Player и 3 типа Opponent. У всех есть HP и damage, но разный способ выбора зон. Что наследовать, что вложить через композицию? Почему?


5 · Game loop и turn-based логика ⭐⭐

Зачем: ход в Not Fight Club — это не «нажал → бой кончился». Это: pick attack → pick 2 defense → confirm → resolve simultaneously → log × 3 → check death → next turn. Без чёткого цикла — баги.

📝 Идея направления:

«Turn = чистая функция: resolveTurn(playerMove, opponentMove, state) → newState. Никаких side effects внутри. Анимации, лог, ре-рендер — снаружи, после того как новый state посчитан.»

Self-check: опиши словами, какие шаги происходят между «игрок нажал кнопку Attack» и «HP-бар двинулся». Какие из них синхронные, какие могут быть async?


6 · Randomization (Math.random + без повторов)

Зачем: опонент должен выбирать зоны рандомно в рамках своего профиля, и не повторять одну зону дважды за ход. Наивный Math.random × 2 раза = баги (с шансом ~33% получишь дубль).

  • Числа -- Number, Math, parseInt, parseFloatMath.random, Math.floor
  • Паттерн «случайные N из массива без повторов»: shuffle + slice (Fisher-Yates), либо «выбрал → убрал из пула → выбрал ещё»
  • Critical hit: Math.random < CRIT_CHANCE (например 0.15 = 15%)

Self-check: напиши функцию pickRandomZones(pool, count) которая возвращает count случайных уникальных зон из pool. Что произойдёт если count > pool.length?


7 · localStorage и полная персистентность ⭐⭐

Зачем: имя/аватар/W-L = базовая персистентность (требование). Бонус +30 = resume in-progress battle: после reload игрок видит тот же бой, те же HP, те же логи, тот же ход.

  • Web Storage -- localStorage и sessionStorage — что это, API, лимиты
  • Web Storage -- localStorage и sessionStorage — разница, когда что
  • JSONJSON.stringify / JSON.parse, что НЕ сериализуется (функции, undefined, Symbol, Date → строка)
  • Архитектурный приём: single source of truth — храни весь state одним объектом под одним ключом, не разбрасывай по 10 ключам

Self-check: что произойдёт если сохранишь объект Player (instance класса) через JSON.stringify, а потом распарсишь — получишь ли обратно Player или просто Object? Как восстановить методы?


8 · Архитектура vanilla большого приложения ⭐⭐

Зачем: один index.js на 1500 строк — это путь к сломанному cross-check'у. Разбей на модули. Не обязательно сразу ESM с bundler'ом — можно <script type="module"> + относительные импорты, браузер сам подтянет.

📝 Идея направления (компоновка, не код):

«Папки по слоям: data/ (opponents.js, zones.js), state/ (store.js, actions.js), ui/ (по одному файлу на screen: registration.js, home.js, ...), core/ (battle.js, logger.js, persistence.js), main.js (точка входа, подписки, роутинг).»

Self-check: нарисуй дерево файлов проекта. Какие модули зависят от каких? Где state? Где данные опонентов? Где функция начисления damage?


9 · Опц. паттерны: State, Observer

Зачем: не обязательно, но если хочешь чистый код — это ровно те паттерны под эту задачу.

Self-check: в чём разница State Pattern и просто if (currentScreen === 'battle')? Когда стоит вводить паттерн, а когда — оверкилл?


10 · Workflow (одинаковый для всех задач)


✅ Чек-лист критериев (300 баллов)

1. Registration screen · 20

  • Input для имени, имя переиспользуется на всех экранах и переживает reload (+20)

2. Home screen · 10

  • Кнопка «Start battle» создаёт новый бой и открывает Battle (+10)

3. Character page · 45

  • Показывает аватар (+10)
  • Показывает имя + W/L record (+10)
  • Можно выбрать новый аватар из набора, выбор отражается везде (+25)

4. Settings page · 20

  • Можно поменять имя, новое имя отражается везде и переживает reload (+20)

5. Battle page · 175

  • Player + Opponent с интерактивными HP-барами (+35)
  • Опонент выбирается из пула ≥ 2 опонентов с разными профилями (+10)
  • Полная battle mechanics: выбор зон, attack/defense matching, simultaneous resolve, рандом без повторов в одном ходу (+25)
  • Critical hits: шанс на extra damage + пробитие блока (+10)
  • Battle log: каждое действие отдельной записью, WHO/WHOM/WHERE/HOW MUCH (+85)
  • В лог-записях визуально подсвечены имена / зоны / damage (+10)

6. Бонус — full persistence · 30

  • После reload сохраняется character + W/L + in-progress battle в том же state (HP, ход, log) (+30)

Penalties

  • HP ≥ 3× damage (иначе −35)
  • Нет UI-фреймворка (иначе −300)
  • Нет console errors при нормальной игре (иначе −15)

🧠 Self-check перед коммитом

Не нажимай git push, пока не сможешь ответить:

  1. Если опонент атакует 2 зоны, а игрок защищает 2 — сколько log-записей появится в этом ходу?
  2. Что произойдёт если у спайдера в профиле «атакует 2 зоны», а зон всего 3? Может ли он по случайности атаковать одну зону дважды?
  3. Где у меня живёт state? Если открою devtools и наберу в консоли — найду его одним объектом или разбросан?
  4. Если я сделаю reload во время боя — что должно сохраниться, чтобы получить +30?
  5. У моего самого слабого fighter'а: HP = ? damage = ? Проходит ли он правило HP ≥ 3× damage?
  6. Сколько у меня файлов в src/? Если больше 10 — есть ли там логическая группировка? Если 1 — почему?
  7. Что произойдёт если игрок нажмёт «Attack» когда выбрал только 1 защитную зону? (должна блокироваться кнопка)
  8. Console чистая? F12 → Console → reload → играю один бой → 0 ошибок и 0 warnings?

➡️ Что переходит в следующие задачи (compound forward)

В HTML Builder (следующая задача) переиспользуешь:

  • Блоки 1-4 (объекты, классы, state, композиция) — там дерево DOM-узлов это тоже state
  • Блок 8 (архитектура модулей) — HTML Builder ещё больше
  • Паттерн «UI = функция от state» — снова

В Async Race:

  • Блок 5 (game loop) → анимация гонки через requestAnimationFrame
  • Блок 7 (localStorage) → персистентность винов / гаража
  • Блок 8 (архитектура) → ещё крупнее проект, без модулей не выживешь
  • Блок 2 (классы) — Car, Garage, Engine напрашиваются классами

В CV / Microservices (финал bootcamp):

  • Опыт turn-based + state даст интуицию для async-flow в Node.js
  • Архитектурный навык «не клади всё в один файл» — переносится 1-в-1

📚 Внешние ресурсы