| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- <?php
- /**
- * Worded maths problems ("word_problems").
- *
- * Rewritten. What was wrong with the old version:
- * - it didn't work like the other generators: no form, no stored answer, no
- * constructor taking the user, and TK2 never called it (the worded_*
- * subjects were mapped to the spelling generator)
- * - answers came from eval() of a text expression and were cut to an integer
- * with (int), so most were wrong: ((7/2)*5) is 17.5, not 17
- * - the sponge problem ignored the sponge's starting mass
- * - difficulty() returned null for a new student, and the "diff" token in the
- * templates broke when it did
- * - `e107.e107_tkReport` hard-coded, user not escaped
- *
- * Now every problem is built backwards from its answer, so the answer is
- * always a whole number, there is no eval(), and the number ranges grow with
- * the student's accuracy over the last month (and with how many they have
- * answered, so one lucky answer doesn't jump to the hardest numbers).
- */
- class math_word_problems {
- const SUBJECT = 'word_problems';
- private $user;
- private $level = 0; // 0-100: how hard to make the numbers
- private $names = ['Anna', 'George', 'Marie', 'Elliot', 'Paul', 'Stacey', 'Maureen', 'Priya', 'Sam', 'Mia'];
- /**
- * @param string $user User to supply the question for.
- */
- public function __construct($user) {
- $this->user = $user;
- }
- /**
- * Make a problem, show it, and remember it as the student's open question.
- */
- public function getWordProblem() {
- $problem = $this->getProblem();
- $questionHtml = $this->htmlify($problem);
- $db = e107::getDb();
- $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
- $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
- e107::getRender()->tablerender('Word problems', $questionHtml);
- }
- /**
- * Make a problem without showing it.
- *
- * @return array ['answer1' => int, 'question' => string, 'type' => 'word_problems']
- */
- public function getProblem() {
- $this->level = $this->difficulty();
- $makers = ['pillowCases', 'sponges', 'elephant', 'sandwiches', 'marbles', 'beach', 'cupcakes', 'sugarBags'];
- $maker = $makers[array_rand($makers)];
- list($question, $answer) = $this->$maker();
- return ['answer1' => (int) $answer, 'question' => $question, 'type' => self::SUBJECT];
- }
- /**
- * How hard to make the numbers, 0-100: last month's accuracy, scaled up
- * over the first 30 answers.
- *
- * @return float
- */
- private function difficulty() {
- $db = e107::getDb();
- $stats = $db->retrieve(
- "SELECT (SUM(correct)/COUNT(correct)*100) AS percent, COUNT(correct) AS count
- FROM `" . MPREFIX . "tkReport`
- WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
- AND username = '" . $db->escape($this->user) . "' AND subject = '" . self::SUBJECT . "'"
- );
- if (!is_array($stats) || (int) $stats['count'] === 0) {
- return 0.0;
- }
- $sample = min(1.0, ((int) $stats['count']) / 30);
- return max(0.0, min(100.0, (float) $stats['percent'])) * $sample;
- }
- /**
- * A random whole number from $min up to $maxAtFull. At level 0 the top of
- * the range is a quarter of the way there; at level 100 it is all of it.
- */
- private function num($min, $maxAtFull) {
- $span = max(0, $maxAtFull - $min);
- $max = $min + (int) ceil($span * (0.25 + 0.75 * $this->level / 100));
- return mt_rand($min, max($min, $max));
- }
- private function name() {
- return $this->names[array_rand($this->names)];
- }
- // ---- the problems: each returns [question text, whole number answer]
- private function pillowCases() {
- $perCase = $this->num(2, 12);
- $made = $this->num(2, 10);
- $wanted = $this->num(2, 20);
- if ($wanted === $made) {
- $wanted++;
- }
- return [
- $this->name() . ' used ' . ($perCase * $made) . ' metres of cloth to sew ' . $made . ' pillow cases. How much cloth would be used to sew ' . $wanted . ' pillow cases?',
- $perCase * $wanted,
- ];
- }
- private function sponges() {
- $mass = $this->num(10, 60);
- $gain = $this->num(2, 30);
- $count = $this->num(2, 12);
- return [
- 'The mass of a sponge is ' . $mass . ' g. When put in water it expanded and its mass became ' . ($mass + $gain) . ' g. How much extra mass would ' . $count . ' similar sponges gain altogether?',
- $gain * $count,
- ];
- }
- private function elephant() {
- $kg = $this->num(10, 150);
- $years = $this->num(2, 10);
- return [
- 'An elephant eats ' . $kg . ' kg of food a week. How many kg will it eat in ' . $years . ' years? (Use 52 weeks in a year.)',
- $kg * 52 * $years,
- ];
- }
- private function sandwiches() {
- $each = $this->num(2, 5);
- $children = $this->num(3, 30);
- return [
- 'A mother makes ' . ($each * $children) . ' sandwiches for a party. Each child receives ' . $each . ' sandwiches. How many children were at the party?',
- $children,
- ];
- }
- private function marbles() {
- $quarter = $this->num(5, 60);
- return [
- 'There were ' . ($quarter * 4) . ' marbles in a jar. ' . $this->name() . ' dropped the jar and lost 3 quarters of the marbles. How many marbles were left?',
- $quarter,
- ];
- }
- private function beach() {
- $fifth = $this->num(10, 200);
- return [
- 'At the beach there were ' . ($fifth * 5) . ' people. One fifth were children and the rest were adults. How many adults were at the beach?',
- $fifth * 4,
- ];
- }
- private function cupcakes() {
- $fifth = $this->num(10, 100);
- return [
- $this->name() . ' baked ' . ($fifth * 5) . ' cupcakes and sold 2 fifths of them at the market. How many cupcakes were left?',
- $fifth * 3,
- ];
- }
- private function sugarBags() {
- $perBag = $this->num(2, 20);
- $bags = $this->num(2, 10);
- return [
- $this->name() . ' has to pack ' . ($perBag * $bags) . ' kg of sugar equally into ' . $bags . ' bags. How many kg of sugar will each bag contain?',
- $perBag,
- ];
- }
- private function htmlify($problem) {
- // Expected answer stays on the server; the form only carries a token.
- $token = TkAnswerStore::issue($this->user, $problem);
- $frm = e107::getForm();
- $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
- $fString .= $frm->label('<h4>Word problems</h4>') . "</br>\n";
- $fString .= $frm->label('Read the problem and write just the number as your answer.') . "\n";
- $fString .= '</br>';
- $fString .= '<p style="font-size:120%;">' . htmlspecialchars($problem['question']) . '</p>';
- $fString .= '</br>';
- $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
- $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
- $fString .= $frm->hidden('subject', self::SUBJECT, ['id' => 'subject']);
- $fString .= '</br>' . "\n";
- $fString .= $frm->button('Answer', 'Check answer') . "\n";
- $fString .= $frm->close();
- $fString .= '</br></br>';
- return $fString;
- }
- }
|