Тесты: итоговый балл, набор карточек и создание
Подробное описание того, как система считает итоговый балл за завершённый прогон теста, откуда берётся список карточек в тесте и как учителю создать тест из урока. Текст согласован с реализацией в приложении EasyA (API и веб‑интерфейс).
Содержание
- Связанные материалы
- Итоговый балл за прогон теста
- Как формируется набор карточек
- Как создать тест (учитель)
- Ограничения и особые случаи
Связанные материалы
- Тесты (обзор для учителя) — запуск, экран теста, результаты.
- Тесты (обзор для ученика) — список тестов и self‑тест.
Итоговый балл за прогон теста
После завершения прогона теста (все карточки пройдены, тест закрыт учителем/по правилам или истёк) сервер пересчитывает прогресс карточек и вычисляет итоговый балл прогона для каждого участника. В коде 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 (подбор карточек «как в превью»).
Три сценария для теста из урока
-
Групповой тест (
target_type = group): для каждого ученика из урока (в порядке: активные члены выбранной группы, затем остальные участники урока) вызывается тот же алгоритм превью, что и в API. Карточки могут создаваться с привязкой к записи участника (participant_id). При включённом исключении одинаковых слов между учениками последующим ученикам не попадают слова (word_id), уже занятые карточками других. При более двух участников на группу действует общий бюджет карточек на превью/сборку (делится между учениками), чтобы набор оставался разумным по размеру. -
Индивидуальный тест, и клиент передал
preview_cards(снимок экрана предпросмотра): порядок и пары «способ показа / формат ответа» берутся как в снимке. Сервер проверяет, что каждая карточка принадлежит участнику урока, что пара разрешена матрицей заданий и что навык согласован с настройками. При необходимости навык восстанавливается из полей превью или подбирается по самому слабому применимому навыку среди включённых в тест (по прогрессу на карточке). -
Индивидуальный тест без снимка: подбор выполняется через
TestPreviewServiceдля выбранногоpreview_user_id(если указан и это участник урока) или для первого участника. Полученный список записывается вtest_itemsбез привязки к конкретному участнику (один общий список на тест для целевого ученика/режима).
Алгоритм подбора одной карточки в превью (упрощённо)
Для заданного пользователя и целевого числа карточек:
- Из включённых способов показа и форматов ответа выводится множество допустимых навыков проверки.
- Навыки перемешиваются.
- Для каждого навыка обходятся пороги доли прогресса (из настроек пользователя): для каждого диапазона есть «квота» карточек из этого диапазона в тесте.
- Выполняется запрос к базе: карточки пользователя с прогрессом по колонке навыка в нужном диапазоне, с фильтрами (теги, атрибуты слова), с исключением уже выбранных и явно заданных id, с ограничениями по медиа (например, для listening нужны и перевод, и аудио у слова). Сортировка: сначала более слабый прогресс, затем давно не тестировавшиеся, стабильный порядок по id.
- Если карточка не найдена и разрешён «мягкий» режим (
relax_filter), последовательно ослабляются фильтры (сначала атрибуты, затем теги) и поиск повторяется. - Если квоты порогов исчерпаны, выполняется второй проход без жёсткой квоты порога (
anyThreshold), чтобы заполнить тест. - Для найденной карточки из допустимых пар «способ показа — формат ответа» для выбранного навыка случайно выбирается одна комбинация (в пределах настроек теста).
Для 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).
Пошагово
- Откройте нужный урок и перейдите на экран создания теста по ссылке выше (или кнопке из урока, если она настроена в вашей сборке).
- Выберите для кого тест: конкретный ученик, группа или весь состав урока — в зависимости от доступных режимов.
- Настройте цели теста (навыки), как показывается задание, форматы ответа, подсказки и транскрипцию, автопроверку, при необходимости фильтры (теги, атрибуты слов), число карточек или явный список, режим завершения (классический и др., если доступны).
- Дождитесь предпросмотра: список карточек подгружается с сервера (
POST /tests/previewотносительно базового URL API). При несовместимых настройках превью может быть недоступно — интерфейс подскажет, чего не хватает (например, не выбраны способ показа и формат ответа). - Нажмите действие создать тест. Клиент вызывает
POST /testsсlesson_idи объектомsettings(как в превью, плюс поля снимка для учителя). После успешного ответа выполняется переход на экран прогона:/tests/{testId}(числовой id созданной записи теста). - Запуск теста для учеников выполняется уже с экрана урока/теста в соответствии с обзорной инструкцией.
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.