Перейти к содержанию

Тесты: итоговый балл, набор карточек и создание

Подробное описание того, как система считает итоговый балл за завершённый прогон теста, откуда берётся список карточек в тесте и как учителю создать тест из урока. Текст согласован с реализацией в приложении EasyA (API и веб‑интерфейс).

Содержание

Связанные материалы


Итоговый балл за прогон теста

После завершения прогона теста (все карточки пройдены, тест закрыт учителем/по правилам или истёк) сервер пересчитывает прогресс карточек и вычисляет итоговый балл прогона для каждого участника. В коде API это делает сервис TestResultsScoreService (полное имя класса: App\Services\Tests\TestResultsScoreService). Он вызывается при завершении теста вместе с применением прогресса по ответам.

Примечание: в переписках иногда фигурирует название «TestRunResultsScoreService» — в текущей кодовой базе EasyA используется именно TestResultsScoreService.

Общая идея

Итоговый балл отражает сразу несколько факторов:

  • Насколько хорошо отвечали по целям (строкам ответа), с учётом подсказок там, где они уменьшают «идеальный» балл.
  • Объём теста (число карточек в наборе участника): очень короткие тесты получают меньший множитель «объёма».
  • Скорость относительно ожидаемого времени на такой объём (слишком долго — балл снижается; слишком быстро — в пределах настроек может быть бонус до потолка).
  • Полнота проверки: если часть ответов так и не получила отметку верно/неверно (например, досрочное завершение с «дырами»), на балл действует коэффициент завершённости.

Значение ограничено сверху параметром cap (по умолчанию до 110 баллов, то есть выше «условных 100%» возможен небольшой запас за скорость и объём).

Формула (в терминах системы)

Обозначения:

  • N — число карточек (test_items) в наборе участника (для группового теста у каждого свой набор и свой N).
  • A — доля набранных «сырых» баллов по целям от максимума: (A = S_{\mathrm{raw}} / S_{\mathrm{max}}), где (S_{\mathrm{max}} = 2 \cdot G), а G — число строк ответов (test_answers), у которых уже выставлено is_correct (не null).
  • За одну цель при верном ответе начисляется 2 балла в числитель, при верном с подсказкой1, если подсказка учитывается для этого навыка (см. ниже). Неверно — 0 в числитель (но строка всё равно «оценена» и входит в G).
  • A′ = nonlinearQualityRatio(A)нелинейное преобразование доли A на отрезке [0, 1]: первые проценты правильных ответов дают больший вклад в итог, чем последние (лёгкий старт, «вес» единицы роста A убывает к 100 %). Кривая кусочно‑линейная, монотонная, с f(0)=0, f(1)=1.
  • V(N) — множитель объёма:
    [ V(N) = \frac{N/(N+k_V)}{N_{\mathrm{ref}}/(N_{\mathrm{ref}}+k_V)} ]
    при корректных положительных (N_{\mathrm{ref}}), (k_V). Так нормируется «полезный объём» относительно опорного числа карточек.
  • T — длительность прогона в минутах (из сохранённой длительности теста или разницы started_at / ended_at, с нижней границей по минутам, чтобы не делить на ноль).
  • T_exp — ожидаемое время на такой объём:
  • индивидуально: (T_{\mathrm{exp}} = T_{\mathrm{ref}} \cdot (N / N_{\mathrm{ref}}));
  • групповой тест: (T_{\mathrm{exp}} = T_{\mathrm{ref}} \cdot (N / N_{\mathrm{ref}}) \cdot m), где m — число участников, для которых реально есть свои карточки с привязкой к участнику (см. логику «ведра» карточек в сервисе).
  • S — множитель скорости:
    [ S = \mathrm{clamp}(s_{\min},\ 1+\delta,\ (T_{\mathrm{exp}}/T)^{p}) ]
    для индивидуального прогона используются p и delta; для группового — отдельные p_group и delta_group (обычно чуть мягче/иные по конфигурации).
  • C — доля строк ответа, по которым уже выставлено is_correct (от всех строк ответов в наборе): штраф за незавершённую оценку при досрочном завершении.

Итог:

[ \textbf{Score} = \min(\texttt{cap},\ 100 \cdot A' \cdot V(N) \cdot S \cdot C) ]

Результат округляется до двух знаков после запятой и сохраняется вместе с метаданными расчёта (промежуточные A, A′, V, S, C, N, T, T_exp и т.д.) для разбора спорных случаев.

Подсказка и «сырые» баллы за цель

Подсказка уменьшает вклад только для навыков перевод (translation) и произношение (pronunciation): при верном ответе с подсказкой в числитель идёт 1 вместо 2. Для остальных навыков при верном ответе в числитель идёт 2, независимо от флага подсказки на карточке (как в логике применения прогресса по тесту).

Константы по умолчанию

Параметры задаются конфигурацией tests.results_score (в т.ч. через переменные окружения с префиксом TEST_RESULTS_SCORE_). Имеют смысл опорные N_ref, T_ref (минуты), k_V, показатели степени p / p_group, отклонения delta / delta_group, нижняя граница скорости s_min, потолок cap, малая добавка к длительности epsilon_minutes.

Точные значения по умолчанию смотрите в файле v3.api.easya/config/tests.php в репозитории приложения.

Когда балл не выставляется

  • Нет участников или нет оценённых ответов (G = 0) — расчёт для участника невозможен.
  • Не удаётся определить длительность (T) — у всех участников в метаданных фиксируется причина ошибки.
  • У участника нет карточек в его «ведре» — балл обнуляется/помечается как пропуск с пояснением в метаданных.
flowchart TD
  A[Тест завершён] --> B[TestCompleteService]
  B --> C[Прогресс карточек]
  B --> D[TestResultsScoreService]
  D --> E{Для каждого участника}
  E --> F[Собрать карточки и ответы]
  F --> G[Посчитать A, A_prime, V, S, C]
  G --> H[Score и meta в БД]

Как формируется набор карточек

Набор фиксируется в момент создания прогона теста (черновик со списком test_items). Дальше порядок и состав этого списка — основа прохождения. Логика сосредоточена в TestBuildService (создание записи теста и позиций) и TestPreviewService (подбор карточек «как в превью»).

Три сценария для теста из урока

  1. Групповой тест (target_type = group): для каждого ученика из урока (в порядке: активные члены выбранной группы, затем остальные участники урока) вызывается тот же алгоритм превью, что и в API. Карточки могут создаваться с привязкой к записи участника (participant_id). При включённом исключении одинаковых слов между учениками последующим ученикам не попадают слова (word_id), уже занятые карточками других. При более двух участников на группу действует общий бюджет карточек на превью/сборку (делится между учениками), чтобы набор оставался разумным по размеру.

  2. Индивидуальный тест, и клиент передал preview_cards (снимок экрана предпросмотра): порядок и пары «способ показа / формат ответа» берутся как в снимке. Сервер проверяет, что каждая карточка принадлежит участнику урока, что пара разрешена матрицей заданий и что навык согласован с настройками. При необходимости навык восстанавливается из полей превью или подбирается по самому слабому применимому навыку среди включённых в тест (по прогрессу на карточке).

  3. Индивидуальный тест без снимка: подбор выполняется через TestPreviewService для выбранного preview_user_id (если указан и это участник урока) или для первого участника. Полученный список записывается в test_items без привязки к конкретному участнику (один общий список на тест для целевого ученика/режима).

Алгоритм подбора одной карточки в превью (упрощённо)

Для заданного пользователя и целевого числа карточек:

  1. Из включённых способов показа и форматов ответа выводится множество допустимых навыков проверки.
  2. Навыки перемешиваются.
  3. Для каждого навыка обходятся пороги доли прогресса (из настроек пользователя): для каждого диапазона есть «квота» карточек из этого диапазона в тесте.
  4. Выполняется запрос к базе: карточки пользователя с прогрессом по колонке навыка в нужном диапазоне, с фильтрами (теги, атрибуты слова), с исключением уже выбранных и явно заданных id, с ограничениями по медиа (например, для listening нужны и перевод, и аудио у слова). Сортировка: сначала более слабый прогресс, затем давно не тестировавшиеся, стабильный порядок по id.
  5. Если карточка не найдена и разрешён «мягкий» режим (relax_filter), последовательно ослабляются фильтры (сначала атрибуты, затем теги) и поиск повторяется.
  6. Если квоты порогов исчерпаны, выполняется второй проход без жёсткой квоты порога (anyThreshold), чтобы заполнить тест.
  7. Для найденной карточки из допустимых пар «способ показа — формат ответа» для выбранного навыка случайно выбирается одна комбинация (в пределах настроек теста).

Для self‑теста ученика используется тот же TestPreviewService, но без урока: один участник, настройки из формы создания.

Связь интерфейса учителя с API

Клиент SPA ходит в API с базовым URL, который уже включает префикс /api (см. VITE_API_URL / apiClient в v3.spa.easya). Относительно этой базы вызываются POST /tests/preview (предпросмотр) и POST /tests (создание прогона). В адресной строке браузера при этом часто виден полный путь вида /api/tests/preview на том же хосте, что и API.

На странице создания теста из урока интерфейс строит то же тело запроса, что и для превью, и по мере изменения настроек запрашивает его. При создании теста в POST /tests передаются lesson_id (для теста из урока) и объект settings — тот же набор полей плюс, при наличии, preview_user_id и preview_cards, чтобы итоговый тест совпадал с показанным превью.


Как создать тест (учитель)

Путь в интерфейсе

Путь: Уроки → карточка урока → создание теста.

  • Маршрут в приложении: /lessons/{lessonId}/tests/create, где {lessonId} — числовой id урока (как параметр :lessonId в роутере SPA).

Пошагово

  1. Откройте нужный урок и перейдите на экран создания теста по ссылке выше (или кнопке из урока, если она настроена в вашей сборке).
  2. Выберите для кого тест: конкретный ученик, группа или весь состав урока — в зависимости от доступных режимов.
  3. Настройте цели теста (навыки), как показывается задание, форматы ответа, подсказки и транскрипцию, автопроверку, при необходимости фильтры (теги, атрибуты слов), число карточек или явный список, режим завершения (классический и др., если доступны).
  4. Дождитесь предпросмотра: список карточек подгружается с сервера (POST /tests/preview относительно базового URL API). При несовместимых настройках превью может быть недоступно — интерфейс подскажет, чего не хватает (например, не выбраны способ показа и формат ответа).
  5. Нажмите действие создать тест. Клиент вызывает POST /tests с lesson_id и объектом settings (как в превью, плюс поля снимка для учителя). После успешного ответа выполняется переход на экран прогона: /tests/{testId} (числовой id созданной записи теста).
  6. Запуск теста для учеников выполняется уже с экрана урока/теста в соответствии с обзорной инструкцией.
sequenceDiagram
  participant U as Учитель
  participant SPA as Веб‑приложение
  participant API as API EasyA

  U->>SPA: Настройки на /lessons/:lessonId/tests/create
  SPA->>API: POST /tests/preview (к базе …/api)
  API-->>SPA: Список карточек превью
  U->>SPA: Создать тест
  SPA->>API: POST /tests (lesson_id, settings)
  API-->>SPA: id теста (testId)
  SPA->>U: Переход на /tests/:testId

Самостоятельный тест ученика

Ученик может создавать свой тест с отдельной страницы (см. Тесты для ученика); там вызывается POST /tests без lesson_id (только settings в теле). Набор карточек формируется по тем же правилам превью, но в рамках одного пользователя и упрощённых настроек интерфейса.


Ограничения и особые случаи

  • Итоговый балл считается только после завершения прогона; в черновике и во время прохождения отображаемый «живой» прогресс карточек — отдельная механика (см. комментарии к progress_points в конфиге тестов в API).
  • Если тест групповой, формулы времени и множители p / delta могут отличаться от индивидуального прогона; объём N и набор ответов считаются по карточкам этого участника.
  • Предпросмотр ограничен по числу карточек сверху (константа превью в API); при больших группах бюджет карточек на человека уменьшается, чтобы суммарный размер оставался в разумных пределах.
  • Создание теста из урока автоматически завершает предыдущие активные/черновые прогоны этого урока (они переводятся в завершённые) — см. логику в TestBuildService.

Ссылки

  • Исходный код (для разработчиков): v3.api.easya/app/Services/Tests/TestResultsScoreService.php, TestBuildService.php, TestPreviewService.php, TestCompleteService.php.
  • Описание OpenAPI (если собрано в вашей среде): v3.api.easya/docs/openapi.yaml — операции в разделе tests.