elements2.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. <?php
  2. /**
  3. * Periodic table question generator.
  4. *
  5. * Changes from the original:
  6. * - element index could reach 118 at 300+ answers (the table has indexes
  7. * 0-117); now capped at the table size
  8. * - difficulty() always returns ['percent' => .., 'count' => ..] (it returned
  9. * int 1 for new users and the code then indexed it like an array)
  10. * - the question type is chosen without float arguments to rand()
  11. * - the stored `question` is now a readable string ("element 12, missing
  12. * symbol") instead of a nested array, which the checker imploded into
  13. * "Array" in its messages; the element data the form needs is stored
  14. * separately (server-side, via TkAnswerStore)
  15. * - periodicTable.json and the template are loaded by absolute path
  16. * - user escaped in SQL; `e107.` database name replaced by MPREFIX
  17. * - previous tkLastQuestion row removed before inserting the new one
  18. */
  19. class elements2 {
  20. private $user;
  21. /** Human wording for each element property, used in the checker's messages. */
  22. private $labels = [
  23. 'symbol' => 'symbol',
  24. 'phase' => 'phase',
  25. 'name' => 'name',
  26. 'number' => 'atomic number',
  27. 'atomic_mass' => 'atomic mass',
  28. ];
  29. /**
  30. * @param string $user User to supply the question for.
  31. */
  32. public function __construct($user) {
  33. $this->user = $user;
  34. }
  35. public function getElements() {
  36. $difficulty = $this->difficulty();
  37. // 0..4, widening as accuracy improves (one more type per 20%).
  38. $maxType = min(4, (int) floor($difficulty['percent'] / 20));
  39. $question = $this->elementSwitch(mt_rand(0, $maxType), $difficulty);
  40. if ($question === null) {
  41. e107::getRender()->tablerender('Elements', 'The periodic table data could not be loaded.');
  42. return;
  43. }
  44. $questionHtml = $this->htmlify($question);
  45. $db = e107::getDb();
  46. $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
  47. $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
  48. e107::getRender()->tablerender('Elements', $questionHtml);
  49. }
  50. private function elementSwitch($switchValue, $difficulty) {
  51. switch ($switchValue) {
  52. case 0:
  53. return $this->elementValues('symbol', $difficulty);
  54. case 1:
  55. return $this->elementValues('phase', $difficulty);
  56. case 2:
  57. return $this->elementValues('name', $difficulty);
  58. case 3:
  59. return $this->elementValues('number', $difficulty);
  60. default:
  61. return $this->elementValues('atomic_mass', $difficulty);
  62. }
  63. }
  64. /**
  65. * Difficulty based on the last month of answers.
  66. *
  67. * @return array ['percent' => float 0-100, 'count' => int]
  68. */
  69. private function difficulty() {
  70. $db = e107::getDb();
  71. $stats = $db->retrieve(
  72. "SELECT (SUM(correct)/COUNT(correct)*100) AS percent, COUNT(correct) AS count
  73. FROM `" . MPREFIX . "tkReport`
  74. WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
  75. AND username = '" . $db->escape($this->user) . "' AND subject = 'elements'"
  76. );
  77. if (!is_array($stats) || (int) $stats['count'] === 0) {
  78. return ['percent' => 0, 'count' => 0];
  79. }
  80. return ['percent' => (float) $stats['percent'], 'count' => (int) $stats['count']];
  81. }
  82. private function loadElements() {
  83. $path = dirname(__DIR__, 2) . '/resources/periodicTable.json';
  84. $json = is_file($path) ? json_decode(file_get_contents($path), true) : null;
  85. return (is_array($json) && !empty($json['elements'])) ? $json['elements'] : null;
  86. }
  87. private function elementValues($sign, $difficulty) {
  88. $count = (int) $difficulty['count'];
  89. if ($count >= 300) {
  90. $incrementalNumber = mt_rand(1, 100);
  91. } elseif ($count >= 200) {
  92. $incrementalNumber = mt_rand(1, 50);
  93. } elseif ($count >= 100) {
  94. $incrementalNumber = mt_rand(1, 20);
  95. } else {
  96. $incrementalNumber = mt_rand(1, 10);
  97. }
  98. $elements = $this->loadElements();
  99. if ($elements === null) {
  100. return null;
  101. }
  102. $selectedElement = mt_rand(0, min((int) ($incrementalNumber * 1.18), count($elements) - 1));
  103. $element = $elements[$selectedElement];
  104. return [
  105. 'answer1' => $element[$sign],
  106. 'question' => 'element ' . $element['number'] . ', missing ' . $this->labels[$sign],
  107. 'type' => 'elements',
  108. 'element' => $element,
  109. 'sign' => $sign,
  110. ];
  111. }
  112. private function htmlify($question) {
  113. // Expected answer stays on the server; the form only carries a token.
  114. $token = TkAnswerStore::issue($this->user, $question);
  115. $frm = e107::getForm();
  116. $tp = e107::getParser();
  117. $sc = e107::getScBatch('timekeeper', TRUE);
  118. $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
  119. $fString .= $frm->label('<h4>Elements</h4>') . "</br>\n";
  120. $fString .= $frm->label('Periodic Table of Elements. Fill in the missing information.') . "\n";
  121. $fString .= '</br>';
  122. $elementData = $question['element'];
  123. $elementData[$question['sign']] = '???';
  124. $sc->setVars($elementData);
  125. $fString .= $tp->parseTemplate(file_get_contents(dirname(__DIR__, 2) . '/templates/elementTable.html'), true, $sc);
  126. $fString .= '</br></br>';
  127. $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
  128. $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
  129. $fString .= $frm->hidden('subject', 'elements', ['id' => 'subject']);
  130. $fString .= '</br>' . "\n";
  131. $fString .= $frm->button('Answer', 'Check answer') . "\n";
  132. $fString .= $frm->close();
  133. $fString .= '</br></br>';
  134. return $fString;
  135. }
  136. }