Недели 7-8 · Not Fight Club
🎯 Что строим
Turn-based браузерная игра в духе Бойцовского клуба — пять экранов, бой по раундам с зонами атаки и защиты, опонент-пул, critical hits, battle log, и полная персистентность в localStorage.
Это самый большой vanilla JS проект bootcamp до Async Race. Тут уже не «фичи на странице» — тут архитектура: state, render, event handling, переходы между экранами, сохранение прогресса. Если влепить всё в один main.js спагетти-стилем — задохнёшься на 5-й фиче.
5 экранов:
- Registration — ввод имени игрока
- Home — кнопка «Start battle»
- Character — аватар, имя, win/loss, смена аватара
- Settings — смена имени
- 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).
🏷 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 у тебя должно быть:
- _MOC DOM · Делегирование событий · События мыши -- click, mouseover, mouseenter
- Массивы · Методы массивов -- map, filter, reduce
- Деструктуризация · spread и rest
- querySelectorAll и NodeList
Если что-то из этого «плыву» — вернись и закрой, иначе на этом проекте утонешь.
1 · JavaScript объекты и работа с ними ⭐
Зачем: игрок, опонент, профиль атаки, лог-запись — всё это объекты. Будешь читать их свойства, мутировать (или копировать — спорный вопрос), сериализовать в JSON.
- JS объекты основы — литералы, доступ к свойствам, методы
- Property Descriptors
- Объекты
- Spread и Rest
- Spread и Rest ← важно когда сохраняешь state
- Деструктуризация объектов
Self-check: объясни разницу Object.assign({}, obj) и structuredClone(obj). Что произойдёт если ты сохранишь в localStorage объект с вложенным массивом и потом изменишь оригинал?
2 · Классы и конструкторы ⭐
Зачем: Player, Opponent, Battle, Logger — это классы. Можно и через factory-функции (см. блок 4), но классы дают чище API: player.takeDamage(20), battle.startTurn.
- Классы —
class,constructor, методы,this - Конструкторы — старый стиль
function Player() {}+ почему классы лучше - Property Descriptors
- Классы
- Приватные поля (
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 функциям.»
- Прочти концептуально: статья State Management in Vanilla JS — DEV (в ресурсах задания)
- Чистые функции — почему
updateStateдолжна возвращать новый state, а не мутировать - Замыкания (Closures) — для создания приватного store без классов
- Observer Pattern — pub/sub чтобы view подписывался на изменения state
Self-check: нарисуй схему: «player нажал кнопку attack → ... → HP-бар обновился». Сколько шагов? Где state живёт? Кто его меняет? Кто узнаёт об изменении?
4 · ООП: композиция vs наследование ⭐
Зачем: велик соблазн сделать class Player extends Fighter и class Opponent extends Fighter. Это работает, но через ~3 фичи упрёшься в diamond problem. Композиция чаще выигрывает.
- Наследование —
extends,super, prototype chain - Композиция функций (pipe, compose) — собираем поведение из мелких объектов/функций
- Композиция функций (pipe, compose) — функциональный подход
- Сравнение: когда что выбрать (в задаче — оба профиля fighter'а имеют одинаковую
takeDamage, но разный AI выбора зон → композиция через стратегию)
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 посчитан.»
- Прочти: Designing Turn-Based Combat — Game Developer (в ресурсах задания)
- Прочти: Game Programming Patterns — Robert Nystrom (особенно главы Game Loop, Update Method, State)
- Чистые функции — почему
resolveTurnдолжна быть чистой - State Pattern — для переключения «выбор зон → resolve → death-screen»
Self-check: опиши словами, какие шаги происходят между «игрок нажал кнопку Attack» и «HP-бар двинулся». Какие из них синхронные, какие могут быть async?
6 · Randomization (Math.random + без повторов)
Зачем: опонент должен выбирать зоны рандомно в рамках своего профиля, и не повторять одну зону дважды за ход. Наивный Math.random × 2 раза = баги (с шансом ~33% получишь дубль).
- Числа -- Number, Math, parseInt, parseFloat —
Math.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 — разница, когда что
- JSON —
JSON.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(точка входа, подписки, роутинг).»
- Модули -- import и export —
export/import, named vs default - Модули -- ES Modules в браузере —
<script type="module">, что меняется - Декомпозиция задач — soft skill: как резать большую задачу на куски
- Routing не требуется (сказано в README) — переключение экранов можно делать через
display: none+ смену активного класса на<body>или через простойhashchangelistener
Self-check: нарисуй дерево файлов проекта. Какие модули зависят от каких? Где state? Где данные опонентов? Где функция начисления damage?
9 · Опц. паттерны: State, Observer
Зачем: не обязательно, но если хочешь чистый код — это ровно те паттерны под эту задачу.
- State Pattern — экраны как состояния FSM (Registration → Home → Battle → Death → Home)
- Observer Pattern — UI подписывается на изменения store
- Композиция функций (pipe, compose) — для цепочки
pickMoves → resolveTurn → log → render
Self-check: в чём разница State Pattern и просто if (currentScreen === 'battle')? Когда стоит вводить паттерн, а когда — оверкилл?
10 · Workflow (одинаковый для всех задач)
- Repo Workflow для bootcamp — публичный репо
not-fight-club, ветки, gh-pages - gh-pages деплой — куда деплоить (если gh-pages не работает — Netlify drop)
- Git Commit Convention —
init:/feat:/fix:+ timestamp в скобках - Conventional Commits — основа конвенции
- PR Description Schema — список фич + что протестировано
- Cross-Check процесс — кто тебя проверяет и по каким критериям
✅ Чек-лист критериев (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, пока не сможешь ответить:
- Если опонент атакует 2 зоны, а игрок защищает 2 — сколько log-записей появится в этом ходу?
- Что произойдёт если у спайдера в профиле «атакует 2 зоны», а зон всего 3? Может ли он по случайности атаковать одну зону дважды?
- Где у меня живёт state? Если открою devtools и наберу в консоли — найду его одним объектом или разбросан?
- Если я сделаю reload во время боя — что должно сохраниться, чтобы получить +30?
- У моего самого слабого fighter'а: HP = ? damage = ? Проходит ли он правило HP ≥ 3× damage?
- Сколько у меня файлов в
src/? Если больше 10 — есть ли там логическая группировка? Если 1 — почему? - Что произойдёт если игрок нажмёт «Attack» когда выбрал только 1 защитную зону? (должна блокироваться кнопка)
- 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
📚 Внешние ресурсы
- 📄 Полное задание
- 🎮 Live demo — поиграй, посмотри как ощущается
- JavaScript DOM — MDN
- Introduction to Events — MDN
- Window.localStorage — MDN
- Math.random — MDN
- Game Programming Patterns — Robert Nystrom (free book)
- Designing Turn-Based Combat — Game Developer
- State Management in Vanilla JS — DEV
- JSON.stringify — MDN
- structuredClone — MDN
- Fisher-Yates shuffle — Wikipedia