|
|
@@ -1,179 +1,209 @@
|
|
|
<?php
|
|
|
|
|
|
-/*
|
|
|
- * To change this license header, choose License Headers in Project Properties.
|
|
|
- * To change this template file, choose Tools | Templates
|
|
|
- * and open the template in the editor.
|
|
|
- */
|
|
|
-
|
|
|
/**
|
|
|
- * Description of Math_word_Problems
|
|
|
+ * 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
|
|
|
*
|
|
|
- * @author gregory
|
|
|
+ * 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 {
|
|
|
-
|
|
|
- public $state = "error";
|
|
|
- private $debug = false;
|
|
|
- private $sql;
|
|
|
+
|
|
|
+ const SUBJECT = 'word_problems';
|
|
|
+
|
|
|
private $user;
|
|
|
- private $difficultyValue;
|
|
|
- private $returnData;
|
|
|
+ private $level = 0; // 0-100: how hard to make the numbers
|
|
|
+ private $names = ['Anna', 'George', 'Marie', 'Elliot', 'Paul', 'Stacey', 'Maureen', 'Priya', 'Sam', 'Mia'];
|
|
|
|
|
|
/**
|
|
|
- * Setup new numbers engine
|
|
|
- *
|
|
|
- * @param bool $test Enable debug mode.
|
|
|
+ * @param string $user User to supply the question for.
|
|
|
*/
|
|
|
- public function __construct($test = false) {
|
|
|
-
|
|
|
- if ($test) {
|
|
|
- $this->debug = true;
|
|
|
- }
|
|
|
- $this->sql = e107::getDB();
|
|
|
- $this->user = e107::getUser()->getLoginName();
|
|
|
- $this->state = "ready";
|
|
|
+ public function __construct($user) {
|
|
|
+ $this->user = $user;
|
|
|
}
|
|
|
|
|
|
- public function getProblem() {
|
|
|
+ /**
|
|
|
+ * Make a problem, show it, and remember it as the student's open question.
|
|
|
+ */
|
|
|
+ public function getWordProblem() {
|
|
|
+ $problem = $this->getProblem();
|
|
|
+ $questionHtml = $this->htmlify($problem);
|
|
|
|
|
|
- $this->difficultyValue = $this->difficulty();
|
|
|
-
|
|
|
- if($this->debug) {
|
|
|
- $this->returnData['difficulty'] = $this->difficultyValue;
|
|
|
- }
|
|
|
-
|
|
|
- $this->questionAssembler();
|
|
|
-
|
|
|
- return $this->returnData;
|
|
|
- }
|
|
|
-
|
|
|
- private function questionAssembler() { //return [ 'answer1'=>$solution['/'], 'question'=>$TempQuestion, 'type'=>__CLASS__];
|
|
|
-
|
|
|
- $questionIndexSelected = array_rand($this->questionData());
|
|
|
- $question = $this->questionData()[$questionIndexSelected];
|
|
|
- $this->returnData['question'] = $this->substitutor($question);
|
|
|
- //print_a([__CLASS__.' '.__FUNCTION__=>['rawQuestion'=>$question,'processedQuestion'=>$this->returnData['question']]]);
|
|
|
+ $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);
|
|
|
}
|
|
|
-
|
|
|
- private function questionData() {
|
|
|
- $wordedMath = [
|
|
|
- ['{name1} used {int,1,1,diff} meters of cloth to sew {int,2,1,diff} pillow cases. How much cloth would be used to sew {int,3,2,diff} pillow cases?','(({int,1,1,diff}/{int,2,1,diff})*{int,3,2,diff})'],
|
|
|
- //['A mother makes {int,1,1,diff} sandwiches for a party. Each child received {int,2,1,diff} sandwiches. How many children were at the party?','({int,1,1,diff}/{int,2,1,diff})'],
|
|
|
- ['The mass of a sponge is {int,1,1,diff} g. When put in water it expanded and gained a mass of {int,2,1,diff} g. Find the mass difference if {int,3,1,diff} similar sponges were used in the experiment?','({int,2,1,diff}*{int,3,1,diff})'],
|
|
|
- //['{name1} is {int,1,1,diff} years old. His brother is {int,2,1,diff} months older than him. How old is {name1}’s brother?','({int,1,1,diff}+({int,1,1,diff}/12))'],
|
|
|
- ['An elephant eats {int,1,1,diff} kg of food a week. How much will it eat in {int,2,1,10} years?','(({int,1,1,diff}*52)*{int,2,1,10})'],
|
|
|
-// ['A cake recipe uses 60 g of flour. How many cakes can I bake with 4.2 kg of flour?'],
|
|
|
-// ['On a scale there is 88 grams on side A and 1.3 kg on side B. How much more weight do I need to put on side A to balance the scale?','()'],
|
|
|
-// ['{name1} baked 400 cupcakes and she sold 2 fifths at the market. How many cupcakes does she have left?'],
|
|
|
-// ['A baker baked 720 slices of pizza. He sold 5 nineths of the pizzas and threw away 45 slices. How many slices were left?'],
|
|
|
-// ['A cake recipe uses 1 quarter kilogram of sugar. How many cakes can I bake with 750 grams of sugar?'],
|
|
|
-// ['There were 280 marbles in a jar. {name1} dropped the jar and lost 3 quarters of the marbles. How many marbles were left?'],
|
|
|
-// ['At the beach there were 675 people. One fifth were children and the rest were adults. How many adults were at the beach?'],
|
|
|
-// ['{name1} is 56 years old and has a brother who is 84 months younger. How old is his brother?'],
|
|
|
-// ['{name1} has to pack 28.35 kg of sugar into 10 bags equally. How much sugar would each bag contain?'],
|
|
|
-// ['There was 310.4 kg of sand. It has to be divided into 100 tubs equally. How much sand would each tub contain?'],
|
|
|
-// ['A packet contains 2.39 kg of flour. {name1} bought 1000 packets for her cake shop. How much flour does she have in total?'],
|
|
|
- ];
|
|
|
- return $wordedMath;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Return the difficulty based on previously answered questions.
|
|
|
+ * How hard to make the numbers, 0-100: last month's accuracy, scaled up
|
|
|
+ * over the first 30 answers.
|
|
|
*
|
|
|
- * @return int Difficulty as percent.
|
|
|
+ * @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 . "'"
|
|
|
+ );
|
|
|
|
|
|
- $query = "SELECT (SUM(correct)/COUNT(correct)*100) as percent
|
|
|
- FROM e107.`e107_tkReport` WHERE
|
|
|
- answereddatestamp >= unix_timestamp((NOW()) - INTERVAL 1 MONTH)
|
|
|
- AND username = '$this->user' AND subject = '".__CLASS__."';";
|
|
|
-
|
|
|
- if($this->debug) {
|
|
|
- $this->returnData['difficultySQL'] = $query;
|
|
|
+ if (!is_array($stats) || (int) $stats['count'] === 0) {
|
|
|
+ return 0.0;
|
|
|
}
|
|
|
-
|
|
|
- $this->sql->gen($query);
|
|
|
- while ($row = $this->sql->fetch()) {
|
|
|
- if (is_numeric($row["percent"])) {
|
|
|
- return (int)$row["percent"];
|
|
|
- } else {
|
|
|
- return 1;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+
|
|
|
+ $sample = min(1.0, ((int) $stats['count']) / 30);
|
|
|
+ return max(0.0, min(100.0, (float) $stats['percent'])) * $sample;
|
|
|
}
|
|
|
-
|
|
|
- private function substitutor($question) {
|
|
|
-
|
|
|
- $questionStr = $this->names($question[0]);
|
|
|
- $solutionStr = $this->names($question[1]);
|
|
|
-
|
|
|
- $questionArray = preg_split("/{|}/",$questionStr);
|
|
|
-
|
|
|
- foreach ($questionArray as $questionPart) {
|
|
|
- if($this->contains('int', $questionPart)) {
|
|
|
- $substituteString = $this->ints(explode(',',$questionPart));
|
|
|
- $questionStr = str_replace('{'.$questionPart.'}', $substituteString, $questionStr);
|
|
|
- $solutionStr = str_replace('{'.$questionPart.'}', $substituteString, $solutionStr);
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
- // eval needed
|
|
|
- return [ 'answer1'=>(int)eval("return ".$solutionStr.";"), 'question'=>$questionStr, 'type'=>__CLASS__];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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 names($questionString) {
|
|
|
-
|
|
|
- $freeNames = ['Anna','George','Marie','Elliot','Paul','Stacey','Maureen'];
|
|
|
-
|
|
|
- for ($int = 1; $int <= 10; $int++) {
|
|
|
- $randomName = array_rand($freeNames);
|
|
|
-
|
|
|
- if($this->contains('{name'.$int.'}', $questionString)) {
|
|
|
- $name = $freeNames[$randomName];
|
|
|
-
|
|
|
- $questionString = str_replace('{name'.$int.'}', $name, $questionString);
|
|
|
- unset($freeNames[$randomName]);
|
|
|
- }else{
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- return $questionString;
|
|
|
+
|
|
|
+ private function name() {
|
|
|
+ return $this->names[array_rand($this->names)];
|
|
|
}
|
|
|
|
|
|
-
|
|
|
- private function ints($dataBlock) {
|
|
|
- foreach ($dataBlock as $key=>&$data) {
|
|
|
- if($this->contains('diff', $data)) {
|
|
|
- $data = (int)$this->difficultyValue;
|
|
|
- if($data<$dataBlock[($key-1)]) {
|
|
|
- $data = $dataBlock[($key-1)];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+ // ---- 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 mt_rand($dataBlock[2],$dataBlock[3]);
|
|
|
+
|
|
|
+ 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,
|
|
|
+ ];
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * contains($needle, $haystack)
|
|
|
- *
|
|
|
- * <code>
|
|
|
- * Example;<br/>
|
|
|
- * $haystack = 'Hello how are you';<br/>
|
|
|
- * $needle = 'are';<br/>
|
|
|
- * contains($needle, $haystack); // == true<br/>
|
|
|
- * </code>
|
|
|
- * @param string $needle
|
|
|
- * @param string $haystack
|
|
|
- * @return bool
|
|
|
- */
|
|
|
- function contains($needle, $haystack)
|
|
|
- {
|
|
|
- return strpos($haystack, $needle) !== false;
|
|
|
+
|
|
|
+ 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;
|
|
|
}
|
|
|
-
|
|
|
-}
|
|
|
+}
|