openwebwork / pg

Problem rendering engine for WeBWorK
http://webwork.maa.org/wiki/Category:Authors
Other
45 stars 76 forks source link

Inconsistent/incomplete behaviour for radio button custom answer checkers #942

Open dlglin opened 1 year ago

dlglin commented 1 year ago

This was motivated by https://webwork.maa.org/moodle/mod/forum/discuss.php?d=8369 which asks about custom answer checkers for multiple choice problems.

Consider the following MWE:

DOCUMENT();

loadMacros(
    "PGstandard.pl",    # Standard macros for PG language
    "PGML.pl",          # PGML markup and Math Objects
    "parserRadioButtons.pl",
    "PGcourse.pl",      # Customization file for the course
);

$checker = sub {
    my ($correct,$student) = @_;  # get correct and student MathObjects
    warn "Correct answer is ".$correct;
    warn "Student answer is ".$student;
    return $correct==$student || $student=='B1';
};

$RB = RadioButtons([["First correct","Second correct","First incorrect"]],0,labels=>'ABC');

BEGIN_PGML
Enter a value for [`\pi`].

[_]{$RB->cmp(checker=>$checker)}
END_PGML

ENDDOCUMENT();

There are a couple of concerns:

  1. The warn statements produce inconsistent output. For the student answer what is displayed is the underlying MathObject value (i.e. B0, B1, or B2), whereas for the correct answer the label is displayed by default (i.e. A, B or C in this example). The underlying checking compares the MathObject values, so $correct==$student returns the proper value, but the values that are displayed in the warn statement are confusing.
  2. If the order of the answers is randomized there doesn't appear to be a way to refer to a specific "incorrect" answer. In the above example the first entry in the array is set as the correct choice, so in the checker $correct->Value returns the proper choice after randomization, but 'B1' will refer to answer B in the scrambled choices, which doesn't consistently correspond to any specific answer. I don't see a way to get the index of "Second correct" after the entries have been scrambled.

I don't know that there is going to be an easy way to fix the second issue. I wonder if it's possible to make the labels of the scrambled answers available to the problem author. This could also be useful in solutions. For example for a "which of the following statements is true" type question, the solution could discuss why each incorrect option is false, and refer to them by their labels.

dpvc commented 1 year ago

For your item 1, $correct is the PopUp object, while $student is a string object whose value is B1 or whatever, and so that is what it shows when it is stringified. The stringified value for the PopUp is the label value. You can convert $student to its label via $RB->answerLabel($student) if you want that. It would be possible to make the student answer strings be the same as the labels, but there is no current method to do that when there is randomization involved. One could be added.

For your item 2, the shuffled answers are in $RB->{orderedChoices}, and the indices into the original (flattened) array of choices for the ordered choices are in $RB->{order}. These can be used to unshuffle the choices, but they aren't quite in a form that makes that easy. For example, if you want the label in the ordered choices of the answer in the original (flattened) choice array with index $i, then you could do something like:

$label = $RB->{labels}[(grep { $RB->{order}[$_] == $i } 0 .. $RB->{n} - 1)[0]];

If you want the label for the answer string $s, then

$label = $RB->{labels}[(grep { $RB->{orderedChoices}[$_] eq $s } 0 .. $RB->{n} - 1)[0]];

should do the trick. If you need to look up the labels of a lot of answers, then you might want to make the reverse mapping from $RB->{order}, say

my @reverse;
map {$reverse[$RB->{order}[$_]] = $_] 0 .. $RB->{n} - 1;

so that $reverse[$i] will be the index into shuffled choices of the choice at index $i of the original choice array. Then

$label = $RB->{labels}[$reverse[$i]];

will be the label for the choice at position $i in the original choice array.

drgrice1 commented 1 year ago

One thing that you can do is use the new values option. The values are reordered together with everything else. So you could do

DOCUMENT();

loadMacros('PGstandard.pl', 'PGML.pl', 'parserRadioButtons.pl', 'PGcourse.pl');

$checker = sub {
    my ($correct, $student) = @_;
    return $correct == $student || $student eq 'second correct';
};

$RB = RadioButtons(
    [ [ 'First correct', 'Second correct', 'First incorrect' ] ], 0,
    labels => 'ABC',
    values => ['first correct', 'second correct', 'first incorrect']
);

BEGIN_PGML
[_]{$RB->cmp(checker=>$checker)}
END_PGML

ENDDOCUMENT();

This will work with PG 2.18, but not before of course.

drgrice1 commented 1 year ago

Another thing you could do that I believe would work prior to PG 2.18 is

DOCUMENT();

loadMacros('PGstandard.pl', 'PGML.pl', 'parserRadioButtons.pl', 'PGcourse.pl');

$checker = sub {
    my ($correct, $student) = @_;
    return $correct == $student || $RB->answerChoice($student) eq 'Second correct';
};

$RB = RadioButtons([ [ 'First correct', 'Second correct', 'First incorrect' ] ], 0, labels => 'ABC');

BEGIN_PGML
[_]{$RB->cmp(checker=>$checker)}
END_PGML

ENDDOCUMENT();

Of course these suggestions don't address your concern about warn showing something different for the correct and student answers.