У твоей игры уже есть ссылка. Теперь выбери одно затруднение, сформулируй проверяемое предположение и выпусти небольшую доработку. Важно сохранить связь между тем, что ты наблюдал, тем, что изменил, и тем, что проверишь снова.

Сначала выбери изменение своей игры

Возьми свою игру после публикации и записи плейтеста. Открой их, выбери одно наблюдение и заполни templates/observation.md. Если внешнего игрока не было, проведи проверку с ним или честно отметь самопроверку. Пример ниже вымышленный; для своей игры запиши собственное наблюдение.

Запиши действие, которое удалось заметить: «после второго хода ищет уже выбранную карту» конкретнее, чем «интерфейс неудобный». Затем отдели наблюдение от объяснения: ты ещё не знаешь, связано ли действие с подписью, расположением или незнакомым правилом.

Я наблюдал: ...
Предполагаю причину: ...
Если изменю ... , то в сценарии ... игрок сможет ... .
Замечу это по действию: ...
Правила, которые оставляю прежними: ...
Что проверю после изменения: ...

Выбери одну небольшую правку. Для неё должно быть ожидаемое поведение, которое можно проверить. Новый эффект сам по себе не объясняет, зачем он нужен. Изменение игрового правила допустимо, если это твоя гипотеза: тогда отдельно обнови описание и тесты ожиданий. Не исправляй одновременно пять причин — будет трудно понять результат.

Подсказка 1: пока есть только «игроку непонятно»

Уточни момент: до первого действия, после результата или при новой попытке. Запиши, что именно человек сделал или спросил. Не объясняй ему новый интерфейс до повторной проверки.

Подсказка 2: повторная проверка ещё невозможна

Проверь код и сценарии сам, но пометь эффект для игрока как непроверенный. Это честнее выдуманного успешного результата. Оставь один вопрос для следующего наблюдения.

Пример после своей попытки

Следующая ситуация вымышлена для разбора. В игре «Собери ровно 10» игрок после карт 1 и 3 спрашивает: «Я уже нажал тройку?» Галочка на карте уже есть, но предположим, что отдельная строка выбранных чисел поможет увидеть состояние. Мы не знаем, насколько часто возникает такое затруднение.

Гипотеза: «Если рядом с суммой показать выбранные числа, игрок сможет назвать свой набор после двух ходов без подсказки». Единственная правка интерфейса — выражение 1 + 3 = 4. Правило трёх разных карт на сумму 10 сохраняется. Дальше разберём этот пример. Для своей игры выбери изменение по собственным наблюдениям.

start совпадает с final прошлого комплекта. Открой папку через HTTP и проверь исходы до изменений. В step-01 добавим строку, в step-02 — отдельный инструмент наблюдения переходов. В final тот же код сопровождают обновлённые README и CHANGELOG. Сравнение папок не является подтверждением удобства для настоящего игрока.

Покажем, какие карты выбраны

В index.html перед <div id="cards" добавь абзац. В style.css в конце добавь правило оформления. Оно не меняет размеры карточек и правила партии.

<p id="selection" class="selection" aria-label="Выбранные карты">Карты пока не выбраны.</p>
.selection {
  padding: 10px 12px;
  background: #eee4ff;
  border-radius: 10px;
  overflow-wrap: anywhere;
}

В начале game.js рядом с другими querySelector добавь ссылку на элемент:

const selectionText = document.querySelector("#selection");

Перед render() добавь функцию ниже. Она берёт уже выбранные id из state, находит соответствующие значения карт и собирает текст. Данные берём из состояния игры, а не из галочек на кнопках. Внешний цикл сохраняет порядок выбора, внутренний ищет карту по id.

function renderSelection() {
  const values = [];
  for (const id of state.selectedIds) {
    for (const card of cards) {
      if (card.id === id) values.push(card.value);
    }
  }
  if (values.length === 0) {
    selectionText.textContent = "Карты пока не выбраны.";
  } else {
    selectionText.textContent = values.join(" + ") + " = " + state.total;
  }
}

Метод join(" + ") соединяет значения массива в одну строку с указанным разделителем. Он не складывает числа: сумма уже находится в state.total. После 1 и 3 массив содержит два числа, а текст получается 1 + 3 = 4. Описание метода есть на MDN.

Самой первой строкой внутри существующей render() вызови renderSelection();. Остальной render сохрани. Проверка step-01: 3, затем 1 дают 3 + 1 = 4; выбранная карта не срабатывает второй раз; «Сначала» возвращает подпись о пустом выборе. Комбинации 1+3+6, 6+5 и 4+6 ведут себя как прежде.

Что считать событием партии

Событие — запись о переходе, а не каждый вызов отображения. В этом примере открытие страницы ещё не считается началом игры. start появляется при первом допустимом выборе карты. win или loss — при победе или поражении. restart — когда игрок сбрасывает уже начатую попытку. Пустое нажатие «Сначала» ничего не меняет и событие не создаёт.

ДействиеОжидаемая запись
Открыть страницу, ещё не выбиратьНет событий
Первая допустимая картаОдин start перед изменением суммы
Последующие карты незавершённой партииНовых start нет
Перейти из playing в won/lostОдин win или loss с итоговой суммой
Сбросить после хотя бы одного выбораrestart с состоянием оставленной попытки
Повторный ввод после конца или renderНикаких новых событий

Такой журнал помогает проверить последовательность действий в одной вкладке. Он не считает уникальных людей, удержание или интерес аудитории. Мы не добавляем идентификаторы, имена, сетевые запросы и обязательный сервис аналитики. Записи ограничены двадцатью и исчезают после перезагрузки. Если используешь журнал во время плейтеста, объясни участнику, какие события в нём записываются.

Добавим небольшой локальный журнал

В index.html после кнопки «Сначала», перед закрывающим main, добавь сворачиваемую область. Она закрыта по умолчанию и не мешает обычной партии.

<details class="journal">
  <summary>Локальный журнал этой вкладки</summary>
  <p>До 20 последних событий. После перезагрузки записи исчезнут; на сервер они не отправляются.</p>
  <ol id="event-log"></ol>
  <button id="clear-log" type="button">Очистить журнал</button>
</details>

В style.css добавь оформление. Ограничена только высота длинного списка; игровые кнопки остаются снаружи. Стандартные номера ol скрыты: номер каждого события уже записан в тексте и сохраняет смысл после удаления старых записей.

.journal {
  margin-top: 20px;
  padding: 12px;
  background: white;
  border-radius: 10px;
}

.journal summary {
  cursor: pointer;
}

.journal ol {
  list-style: none;
  padding-left: 0;
  font-size: 14px;
  overflow-wrap: anywhere;
  max-height: 220px;
  overflow-y: auto;
}

В начале game.js, после import, добавь переменные журнала. Это отдельные данные наблюдения; они не определяют победу.

const eventList = document.querySelector("#event-log");
const clearLogButton = document.querySelector("#clear-log");
const eventLimit = 20;
let eventLog = [];
let eventNumber = 0;

Перед handleCard добавь три функции. recordEvent копирует только тип, номер события, число ходов и текущую сумму. push добавляет запись в конец. Если записей стало больше лимита, shift() удаляет одну старейшую с начала; этот метод изменяет массив, как описано на MDN. Список выводится через textContent, без вставки строки как HTML.

function renderJournal() {
  eventList.textContent = "";
  for (const event of eventLog) {
    const item = document.createElement("li");
    item.textContent = event.number + ". " + event.type + ": ходов " + event.moves + ", сумма " + event.total;
    eventList.appendChild(item);
  }
}

function recordEvent(type) {
  eventNumber = eventNumber + 1;
  eventLog.push({ number: eventNumber, type: type, moves: state.moves, total: state.total });
  if (eventLog.length > eventLimit) eventLog.shift();
  renderJournal();
}

function clearJournal() {
  eventLog = [];
  eventNumber = 0;
  renderJournal();
}

Замени только функцию handleCard следующей. Ранний return остаётся первым: отклонённое действие не должно создавать запись. На первом допустимом ходе start фиксируется до изменения состояния. Исход записывается после вычисления правила. Внутри render событий нет.

function handleCard(card) {
  if (state.phase !== "playing" || state.selectedIds.includes(card.id)) return;

  if (state.moves === 0) recordEvent("start");

  state.selectedIds.push(card.id);
  state.total = state.total + card.value;
  state.moves = state.moves + 1;

  state.phase = getOutcome(state.total, state.moves, target, maxMoves);
  if (state.phase === "won") recordEvent("win");
  if (state.phase === "lost") recordEvent("loss");

  render();
}

Замени функцию resetGame. restart фиксируется до нового состояния, чтобы запись показывала оставленную сумму. Повторный сброс пустой попытки журнал не раздувает.

function resetGame() {
  if (state.moves > 0) recordEvent("restart");
  state = createInitialState();
  render();
}

Внизу game.js рядом с однократной регистрацией resetButton добавь:

clearLogButton.addEventListener("click", clearJournal);

Не помещай регистрацию внутрь resetGame. Кнопка очистки удаляет только записи, не сбрасывая саму партию. После завершения step-02 открой область журнала и сверь переходы. Вызов recordEvent из render дал бы лишние дубли при любой перерисовке — поэтому места записи выбраны в обработчиках изменений.

Демонстрация и разбор после своей попытки

Это версия примера с выражением и журналом. Она показывает технический результат гипотезы, а не измеренный эффект у игроков.

Открыть пример отдельно

В solutions/iteration-example.md заполнена только вымышленная гипотеза. Результатов повторного наблюдения там нет — их нельзя получить чтением кода.

Проверим, что события не дублируются

Начни с перезагрузки. Выбери 1, 3, 6: должны появиться только start и win. Нажми «Сначала»: добавится restart. Снова нажми «Сначала»: список не изменится. Выбери 6, 5: появятся новый start и loss. После конца нажатия карт не должны ни менять сумму, ни дописывать события.

Повтори начало и сброс больше десяти раз, чтобы получить свыше двадцати событий. В списке останутся двадцать последних; их номера могут начинаться не с единицы. Очисти журнал во время партии: сумма и выбранные карты останутся. Перезагрузи страницу: исчезнут и записи, и незаписанный игровой прогресс — у этого примера сохранений нет.

В рабочем проекте повтори тесты rules.js из урока 35 и сценарии до/после своей правки. Журнал помогает увидеть порядок событий, но правила и поведение игрока нужно проверять отдельно.

Выпусти новую версию по прежнему адресу

  1. Запиши своё наблюдение и гипотезу отдельно от публичных файлов. Повтори тот же сценарий после правки, не подсказывая заранее решение.
  2. Сохрани проверенное изменение в Git. Обнови README своей игры и короткий CHANGELOG: что изменилось для игрока, почему, что проверено и что ещё неизвестно.
  3. Собери новую release-папку и открой распакованную копию, как в уроке 37. Не включай в неё приватные записи плейтеста.
  4. Обнови тот же источник GitHub Pages. Дождись успешной публикации и открой прежнюю внешнюю ссылку в другом браузере.
  5. Проверь именно изменённое поведение и основные исходы. Запиши версию, коммит, URL и следующий вопрос для наблюдения в templates/release-notes.md.

В примере final обозначен как 1.1 в README и CHANGELOG; номер помогает различать файлы, но сам не подтверждает улучшения. Для своей игры напиши собственные заметки. Если гипотеза не подтвердилась, это тоже результат: сохрани наблюдение и верни или измени правку осознанно.

Что теперь можно утверждать

Учебный результат: ты самостоятельно выбрал правила, объяснил данные и код, нашёл и проверил ошибку, опубликовал игру и обосновал доработку. Если код целиком повторяет пример, это полезная практика, но самостоятельность ещё нужно подтвердить новой ограниченной задачей.

Интерес игроков: несколько наблюдений подсказывают, что проверить дальше. Они не доказывают удержание аудитории или спрос. Локальный журнал одной вкладки не сообщает, сколько разных людей пришло и вернулось.

Заработок: публикация и техническая готовность не обещают доход. Площадки, реклама и коммерческие решения — отдельные необязательные задачи. Разобраться с ними можно после завершения проекта.

Сохрани результаты работы: короткий бриф, свою игру, историю решений и исправлений, внешний адрес, одну доработку и её проверку. Для каждого навыка различай копирование образца, изменение с подсказкой и решение новой задачи по документации. Красивое оформление не заменяет умения объяснить правило, разбить задачу и найти ошибку. Если что-то ещё не получается, вернись к нужной практике — правилам, отладке или проверяемому рефакторингу.