October 12, 2024

Разработка заданий для модуля LightPT

Начальные действия

Чтобы задания проверялись автоматически, необходимо создать Урок с заданиями. Урок представляет собой папку, в которой находятся .pas-файлы - заготовки с формулировками заданий, модуль проверки Tasks.pas, который будет проверять все выполненные задания в уроке, а также специальный файл lightpt.dat. Этот файл может быть пустым или содержать название урока (в кодировке Windows 1251). Название урока не должно содержать пробелов.

Именно файл lightpt.dat является маркером того, что все задания в этой папке будут проходить через невидимую проверяющую систему.

Названия .pas-файлов с заданиями могут содержать в начале имени числа с последующим знаком подчёркивания, которые игнорируются. Например, 01_Пример1.pas и Пример1.pas это одинаковые имена для системы проверки.

Итак, создадим папку Lesson1 и в ней файл lightpt.dat, в котором напишем одну строчку в кодировке Windows 1251: Урок-1. Задания на массивы

Создание задания

Теперь создадим в этой папке первый файл с заданием ArrTask1.pas со следующим содержимым:

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

Создание модуля проверки заданий

Теперь создадим модуль, который будет проверять все выполненные задания в уроке (в олимпиадном программировании такие подпрограммы называются чекерами). Для этого в той же папке создадим файл Tasks.pas со следующим содержимым (важно использовать именно это имя файла):

Вот полный текст проверяющего модуля Tasks:

unit Tasks;

uses LightPT;

procedure CheckTaskT(name: string);
begin
  ClearOutputListFromSpaces; 
  case name of
    'ArrTask1': begin

    end;
  end;
end;

initialization
  CheckTask := CheckTaskT;
end.

Здесь ClearOutputListFromSpaces надо писать всегда - оно удаляет пробелы из вывода, которые добавляет при выводе Print, a.Print и ему подобные.

Теперь в ветви 'ArrTask1' оператора case напишем чекер, проверяющий выполнение задания ArrTask1.

Написание чекера

Нужно понимать, что чекер состоит из следующих основных компонент:

  • Проверка типов, введенных пользователем (процедура CheckData)
  • Считывание ввода пользователя в локальные переменные
  • Решение задания в чекере и получение ответа
  • Проверка соответствия ответа тому, который вывел пользователь (процедура CheckOutput)

Рассмотрим каждый из этих пунктов по-отдельности

Проверка типов, введенных пользователем

Проверка типов, введенных пользователем, осуществляется с помощью процедуры CheckData. Простейшая форма вызова CheckData имеет вид:

var n := 10; 
CheckData(Input := [cInt] * n);

или

var n := 10; 
CheckData([cInt] * n);

Это говорит о том, что мы проверяем, что пользователь ввёл 10 целых чисел (каждое целое число задаётся здесь маркером типа cInt).

Какие ещё варианты задания входных данных могут быть? Их всего пять:

cInt, cRe, cStr, cBool, cChar

Они должны быть объединены в массив типов тем или иным способом, например, с помощью операций сложения массивов и умножения массива на число. Например, выражение

[cInt] * n

задаёт массив из n целых типов, а выражение

[cStr] + [cInt] * n + [cRe] * 2

задаёт массив из одного строкового типа, после которого идёт n целых типов и 2 вещественных.

Если маркер типа умножается на число, его необязательно окаймлять в ||, например:

[cStr] + cInt * n + cRe * 2

Считывание ввода пользователя

Итак, пусть мы написали

var n := 10; 
CheckData(cInt * n);

Это значит, что для решения задания ученик вводит n целых чисел тем или иным способом.

Теперь чтобы проверить задание, мы должны те n значений, которые ввёл ученик, считать во внутренние переменные. Мы это сделаем таким способом:

var a := IntArr(n);

Функция IntArr(n) считывает следующие n данных, которые ввёл пользователь, в массив, рассматривая их как целые. Таким образом, она возвращает массив из n целых.

Какие еще функции можно использовать для считывания ввода пользователя?

Это простейшие:

Int
Re
Boo
Chr
Str

которые считывают один элемент данных и продвигаются вперёд, пары

Int2
Re2

которые возвращают кортежи (пар другого типа нет поскольку это - редкость), а также массивы одного типа из n элементов:

IntArr(n)
ReArr(n)
BooArr(n)
ChrArr(n)
StrArr(n)

Решение задания в чекере и получение ответа

Теперь когда мы считали все данные, которые ввёл пользователь, в переменные, решим задачу так, как должен решать ученик. В данном случае это просто:

var cnt := a.Count(x -> x mod 2 = 0);

Проверка ответа

Теперь необходимо понять, что в заготовке задания мы уже вывели сгенерированный случайный массив и нам лишь надо вывести найденное значение cnt. Для этого воспользуемся процедурой CheckOutput:

CheckOutput(a,cnt);

Всё, текст проверки полностью готов:

procedure CheckTaskT(name: string);
begin
  ClearOutputListFromSpaces;
  case name of
    'ArrTask1': begin
      var n := 10;
      CheckData(Input := cInt * n); // проверка типов
      var a := IntArr(n);                   // считывание ввода
      var cnt := a.Count(x -> x mod 2 = 0); // решение задания
      CheckOutput(a,cnt);                   // проверка ответа
    end;
  end;
end;

Как видим, он совсем простой - состоит из 5 строк.

Как это выглядит со стороны ученика

Ученик, решая ArrTask1, вначале просто запустит заготовку и увидит

Далее он попытается решить неправильно:

И наконец

он решит задание правильно.

Добавление тестов (не дописано)

К этому может добавляться вызов процедуры генерации тестов. Дело в том, что пользователь может попытаться обмануть систему проверки LightPT и ввести данные, на которых задача решается просто.