math_word_problems.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. /**
  3. * Worded maths problems ("word_problems").
  4. *
  5. * Rewritten. What was wrong with the old version:
  6. * - it didn't work like the other generators: no form, no stored answer, no
  7. * constructor taking the user, and TK2 never called it (the worded_*
  8. * subjects were mapped to the spelling generator)
  9. * - answers came from eval() of a text expression and were cut to an integer
  10. * with (int), so most were wrong: ((7/2)*5) is 17.5, not 17
  11. * - the sponge problem ignored the sponge's starting mass
  12. * - difficulty() returned null for a new student, and the "diff" token in the
  13. * templates broke when it did
  14. * - `e107.e107_tkReport` hard-coded, user not escaped
  15. *
  16. * Now every problem is built backwards from its answer, so the answer is
  17. * always a whole number, there is no eval(), and the number ranges grow with
  18. * the student's accuracy over the last month (and with how many they have
  19. * answered, so one lucky answer doesn't jump to the hardest numbers).
  20. */
  21. class math_word_problems {
  22. const SUBJECT = 'word_problems';
  23. private $user;
  24. private $level = 0; // 0-100: how hard to make the numbers
  25. private $names = ['Anna', 'George', 'Marie', 'Elliot', 'Paul', 'Stacey', 'Maureen', 'Priya', 'Sam', 'Mia'];
  26. /**
  27. * @param string $user User to supply the question for.
  28. */
  29. public function __construct($user) {
  30. $this->user = $user;
  31. }
  32. /**
  33. * Make a problem, show it, and remember it as the student's open question.
  34. */
  35. public function getWordProblem() {
  36. $problem = $this->getProblem();
  37. $questionHtml = $this->htmlify($problem);
  38. $db = e107::getDb();
  39. $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
  40. $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
  41. e107::getRender()->tablerender('Word problems', $questionHtml);
  42. }
  43. /**
  44. * Make a problem without showing it.
  45. *
  46. * @return array ['answer1' => int, 'question' => string, 'type' => 'word_problems']
  47. */
  48. public function getProblem() {
  49. $this->level = $this->difficulty();
  50. $makers = ['pillowCases', 'sponges', 'elephant', 'sandwiches', 'marbles', 'beach', 'cupcakes', 'sugarBags'];
  51. $maker = $makers[array_rand($makers)];
  52. list($question, $answer) = $this->$maker();
  53. return ['answer1' => (int) $answer, 'question' => $question, 'type' => self::SUBJECT];
  54. }
  55. /**
  56. * How hard to make the numbers, 0-100: last month's accuracy, scaled up
  57. * over the first 30 answers.
  58. *
  59. * @return float
  60. */
  61. private function difficulty() {
  62. $db = e107::getDb();
  63. $stats = $db->retrieve(
  64. "SELECT (SUM(correct)/COUNT(correct)*100) AS percent, COUNT(correct) AS count
  65. FROM `" . MPREFIX . "tkReport`
  66. WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
  67. AND username = '" . $db->escape($this->user) . "' AND subject = '" . self::SUBJECT . "'"
  68. );
  69. if (!is_array($stats) || (int) $stats['count'] === 0) {
  70. return 0.0;
  71. }
  72. $sample = min(1.0, ((int) $stats['count']) / 30);
  73. return max(0.0, min(100.0, (float) $stats['percent'])) * $sample;
  74. }
  75. /**
  76. * A random whole number from $min up to $maxAtFull. At level 0 the top of
  77. * the range is a quarter of the way there; at level 100 it is all of it.
  78. */
  79. private function num($min, $maxAtFull) {
  80. $span = max(0, $maxAtFull - $min);
  81. $max = $min + (int) ceil($span * (0.25 + 0.75 * $this->level / 100));
  82. return mt_rand($min, max($min, $max));
  83. }
  84. private function name() {
  85. return $this->names[array_rand($this->names)];
  86. }
  87. // ---- the problems: each returns [question text, whole number answer]
  88. private function pillowCases() {
  89. $perCase = $this->num(2, 12);
  90. $made = $this->num(2, 10);
  91. $wanted = $this->num(2, 20);
  92. if ($wanted === $made) {
  93. $wanted++;
  94. }
  95. return [
  96. $this->name() . ' used ' . ($perCase * $made) . ' metres of cloth to sew ' . $made . ' pillow cases. How much cloth would be used to sew ' . $wanted . ' pillow cases?',
  97. $perCase * $wanted,
  98. ];
  99. }
  100. private function sponges() {
  101. $mass = $this->num(10, 60);
  102. $gain = $this->num(2, 30);
  103. $count = $this->num(2, 12);
  104. return [
  105. '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?',
  106. $gain * $count,
  107. ];
  108. }
  109. private function elephant() {
  110. $kg = $this->num(10, 150);
  111. $years = $this->num(2, 10);
  112. return [
  113. 'An elephant eats ' . $kg . ' kg of food a week. How many kg will it eat in ' . $years . ' years? (Use 52 weeks in a year.)',
  114. $kg * 52 * $years,
  115. ];
  116. }
  117. private function sandwiches() {
  118. $each = $this->num(2, 5);
  119. $children = $this->num(3, 30);
  120. return [
  121. 'A mother makes ' . ($each * $children) . ' sandwiches for a party. Each child receives ' . $each . ' sandwiches. How many children were at the party?',
  122. $children,
  123. ];
  124. }
  125. private function marbles() {
  126. $quarter = $this->num(5, 60);
  127. return [
  128. '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?',
  129. $quarter,
  130. ];
  131. }
  132. private function beach() {
  133. $fifth = $this->num(10, 200);
  134. return [
  135. 'At the beach there were ' . ($fifth * 5) . ' people. One fifth were children and the rest were adults. How many adults were at the beach?',
  136. $fifth * 4,
  137. ];
  138. }
  139. private function cupcakes() {
  140. $fifth = $this->num(10, 100);
  141. return [
  142. $this->name() . ' baked ' . ($fifth * 5) . ' cupcakes and sold 2 fifths of them at the market. How many cupcakes were left?',
  143. $fifth * 3,
  144. ];
  145. }
  146. private function sugarBags() {
  147. $perBag = $this->num(2, 20);
  148. $bags = $this->num(2, 10);
  149. return [
  150. $this->name() . ' has to pack ' . ($perBag * $bags) . ' kg of sugar equally into ' . $bags . ' bags. How many kg of sugar will each bag contain?',
  151. $perBag,
  152. ];
  153. }
  154. private function htmlify($problem) {
  155. // Expected answer stays on the server; the form only carries a token.
  156. $token = TkAnswerStore::issue($this->user, $problem);
  157. $frm = e107::getForm();
  158. $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
  159. $fString .= $frm->label('<h4>Word problems</h4>') . "</br>\n";
  160. $fString .= $frm->label('Read the problem and write just the number as your answer.') . "\n";
  161. $fString .= '</br>';
  162. $fString .= '<p style="font-size:120%;">' . htmlspecialchars($problem['question']) . '</p>';
  163. $fString .= '</br>';
  164. $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
  165. $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
  166. $fString .= $frm->hidden('subject', self::SUBJECT, ['id' => 'subject']);
  167. $fString .= '</br>' . "\n";
  168. $fString .= $frm->button('Answer', 'Check answer') . "\n";
  169. $fString .= $frm->close();
  170. $fString .= '</br></br>';
  171. return $fString;
  172. }
  173. }