Работающую игру стало трудно менять: правило победы смешалось с обработкой клика, а одно вычисление повторяется. Вынесем правило в функцию, затем в отдельный JavaScript-модуль. Игрок должен получить тот же результат, что до изменений.
Продолжай собственную игру из прошлого практикума. Ниже разбирается отдельный пример «Собери ровно 10»: три разные карты из 1–6 должны дать сумму 10. Этот пример нужен для объяснения приёмов; его копирование не заменяет самостоятельный проект.
Сначала зафиксируем, что игра должна делать
Нужны своя работающая версия, Git из урока 11 и HTTP-сервер из урока 19. В комплекте start — точная копия примера из own-game-prototype/solutions/reference-game. Если твоя игра уже написана иначе, не подгоняй её под этот файл: выбери похожий небольшой участок своего кода.
Рефакторинг меняет устройство кода, сохраняя правила. Новый тип карты, другая цель и исправление ошибки — отдельные задачи. Если изменить всё сразу, по результату трудно понять, какое изменение вызвало сбой.
| Действия в примере | Ожидаемое поведение |
|---|---|
| 1 → 3 → 6 | 10 третьей картой: победа |
| 1 → 2 → 3 | 6 после трёх карт: проигрыш |
| 6 → 5 | 11: немедленный проигрыш |
| 4 → 6 | 10 после двух карт: партия ещё идёт |
| Нажать выбранную карту повторно | Сумма и число ходов не меняются |
| После результата нажать «Сначала» | 0 очков, три свободных хода, все карты доступны |
Проверь эти случаи до первой правки. Для своей игры запиши минимум успешный путь, завершение без успеха и сброс. Сохрани наблюдение в worksheets/refactoring-plan.md: пустой шаблон входит в комплект.
В терминале своей папки посмотри, какие файлы изменены. Добавляй только файлы этой работы:
git status
git add index.html style.css game.js
git commit -m "Сохраняю рабочую игру перед рефакторингом"
Если Git сообщает, что нечего фиксировать, рабочая версия уже сохранена. Не создавай пустой коммит ради инструкции. Команды выполняются локально: отправлять код на сервер для этого урока не требуется.
Вынесем правило из обработчика клика
В game.js примера handleCard одновременно записывает выбранную карту, увеличивает сумму и определяет исход. Найди этот фрагмент:
if (state.total > target) {
state.phase = "lost";
} else if (state.moves === maxMoves) {
if (state.total === target) state.phase = "won";
else state.phase = "lost";
}
Правило можно проверить без кнопок. В game.js перед handleCard добавь функцию:
function getOutcome(total, moves, target, maxMoves) {
if (total > target) return "lost";
if (moves === maxMoves) {
if (total === target) return "won";
return "lost";
}
return "playing";
}
Она получает четыре числа и возвращает одну из трёх строк. Это чистая функция: при одинаковых аргументах даёт одинаковый результат, не меняет чужие данные и не обращается к DOM. Передача target и maxMoves аргументами важна: все нужные настройки передаются ей явно.
Порядок проверок сохраняем: сначала перебор, затем окончание разрешённых ходов. Значение total = 10 само по себе не означает победу. В исходной игре 4 + 6 — только две карты, поэтому ответ ещё playing. Функция рассчитана на состояние нашего обработчика: неотрицательные числа и число ходов от 0 до maxMoves. Проверку произвольных пользовательских строк мы здесь не добавляем.
Теперь замени найденное условие в handleCard одной строкой:
state.phase = getOutcome(state.total, state.moves, target, maxMoves);
Остальные действия остаются на местах. Обработчик записывает данные, вызывает правило и render. Это step-01. Повтори все шесть сценариев из таблицы. Если игра стала вести себя иначе, не переходи дальше: посмотри аргументы и порядок условий.
Можно отдельно проверить функцию в Console, пока она находится в обычном game.js: getOutcome(10, 2, 10, 3) должно вернуть "playing". После перехода на модули область видимости изменится; на следующем уроке для таких вызовов сделаем отдельную страницу проверок.
Перенесём одну функцию в модуль
Модуль — JavaScript-файл, который явно сообщает, что отдаёт другим файлам и что получает от них. Нам хватит одного rules.js. Не нужны сборщик, npm, классы или папка для каждой функции.
Создай rules.js рядом с game.js. Перенеси туда getOutcome и добавь перед объявлением слово export:
export function getOutcome(total, moves, target, maxMoves) {
if (total > target) return "lost";
if (moves === maxMoves) {
if (total === target) return "won";
return "lost";
}
return "playing";
}
export делает эту функцию доступной для импорта. Удали её прежнее объявление из game.js: два экземпляра правила будут расходиться при последующих правках. В первую строку game.js запиши:
import { getOutcome } from "./rules.js";
import получает именованный экспорт. Имя внутри фигурных скобок должно совпадать с экспортированным именем; это специальный синтаксис импорта, а не создание объекта. ./ означает файл рядом с импортирующим game.js, расширение .js указываем явно. Не начинай путь с /: тогда он будет отсчитываться от корня сайта, а не от папки игры.
В index.html замени единственную строку подключения скрипта:
<!-- Было -->
<script src="game.js" defer></script>
<!-- Стало -->
<script type="module" src="game.js"></script>
type="module" включает правила модулей, в том числе import/export. Такой скрипт сам ожидает разбора HTML, поэтому defer здесь не требуется. rules.js отдельно через второй script не подключаем: его загрузит import. Импортированные имена принадлежат модулю и не становятся свойствами window; не полагайся на их доступность по короткому имени в Console.
Открой игру через HTTP. Двойной щелчок по index.html с адресом file:// может вызвать ошибку ограничений загрузки модулей. Как в уроке 19, запусти из папки выбранной версии py -3.14 -m http.server 8000 --bind 127.0.0.1 в Windows или python3 -m http.server 8000 --bind 127.0.0.1 с установленным Python на macOS/Linux.
Это step-02. В Network должны успешно загружаться game.js и rules.js. Если карточек нет, сначала прочитай Console: проверь type, имя экспорта, ./ и имя файла с учётом регистра. Исправь одну причину, обнови страницу, повтори сценарии.
Посчитаем оставшиеся ходы один раз
В render дважды встречается maxMoves - state.moves: для счётчика и сообщения. Сейчас они одинаковы. Сохраним результат один раз в начале render:
function render() {
const remainingMoves = maxMoves - state.moves;
// Остальной код функции остаётся здесь.
}
Это фрагмент с местом вставки, а не замена всей функции. Далее замени только два использования:
movesLeftText.textContent = remainingMoves;
// В ветке сообщения для продолжающейся партии:
statusText.textContent = "Выбери ещё карт: " + remainingMoves + ". Нужная сумма: " + target + ".";
Получилась final. При каждом render переменная вычисляется из текущего состояния: мы не создаём вторую изменяемую копию счётчика. Данные задачи — target, maxMoves и cards — уже собраны в начале game.js. Не переносим их в пять новых файлов без причины. В Canvas-проекте похожим блоком будут карты levels; начни с отделения таких данных от условий, а не со строительства движка.
Повтори проверки, затем посмотри diff и сохрани именно проверенный результат:
git diff
git add index.html game.js rules.js
git commit -m "Выношу правило исхода в модуль без изменения игры"
git log --oneline -3
Diff должен показывать перенос правила и удаление повтора, а не случайные новые условия. Если последний собственный коммит оказался ошибочным, сначала сохрани отдельно незавершённые изменения и проверь git status. Команда git revert <хеш> создаёт новый коммит с отменой выбранного изменения, сохраняя историю. Подставляй хеш именно своего коммита, не пример из статьи. После отмены снова запускай игру.
Самостоятельно: вынеси ещё одну проверку в функцию
В своей игре выбери одну проверку, которую можно выполнить без DOM: можно ли покупать улучшение, закончена ли партия, допустим ли выбор карты. Запиши её входы, выход и два граничных случая. Вынеси в функцию, проверь прежнее поведение, затем перенеси в один существующий модуль или оставь в файле, если отдельный файл пока не помогает. Не меняй само правило в этом коммите.
Для дополнительной тренировки на образце вынеси защиту phase !== "playing" || selectedIds.includes(card.id) в canChooseCard(phase, selectedIds, cardId). Функция должна возвращать boolean и не изменять массив selectedIds. Это упражнение на приём, а доказательством самостоятельного результата будет аналогичная работа в твоей игре.
Подсказка 1: что передать функции и что получить в ответ
Внутри функции не нужны state, button или render. Передай фазу, массив выбранных id и проверяемый id. true означает, что карту можно выбирать. В обработчике останется ранний return, когда функция вернула false.
Подсказка 2: функция не должна менять массив
includes читает массив, а push меняет его. В функции допустимости нужен только первый. Выбор новой карты должен по-прежнему записываться один раз в handleCard.
Разбор после попытки
В solutions/check-card rules.js экспортирует вторую небольшую функцию. Условие требует playing и отсутствия id в массиве. Проверь новую карту, уже выбранную карту и любую карту после результата, затем объясни, почему функция не добавляет карту сама.
Ответы на частые вопросы
Если код работает, нужно ли его трогать?
Выбирай участок, который мешает следующему изменению: повторяющееся правило, слишком длинный обработчик или непонятное имя. Если конкретной проблемы нет, оставь работающий код и занимайся задачей игры.
С чего начать рефакторинг в большом файле?
Возьми одну небольшую проверку, запиши её входы и результат. Вынеси её в функцию и повтори прежние сценарии. Разделять весь файл за один раз не нужно.
Что важнее: красиво или стабильно?
Сохрани рабочее поведение. Изменение полезно, когда после него легче найти и проверить нужное правило. Количество файлов и коротких функций само по себе ничего не доказывает.
Когда работа завершена
Ты можешь показать, где код повторялся или где правило было смешано с другими действиями, объяснить свою правку и подтвердить, что игра работает по-прежнему. Рабочая версия сохранена отдельным коммитом. Переименование всего проекта, подключение форматтера или увеличение числа файлов сами по себе этого результата не доказывают.
Сохрани полезную привычку: одно изменение — одна причина — одна проверка. Удаляй старый код после проверки всех его использований поиском по проекту. Автоматическое форматирование и линтер могут помогать, но их установка не является условием прохождения этого урока.
Справочно: модули JavaScript, export и import на MDN.