anagrams.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <?php
  2. /**
  3. * Anagram question generator.
  4. *
  5. * Changes from the original:
  6. * - word selection used `year >= difficulty`, i.e. beginners got every word
  7. * and strong players only the hardest; now `year <= difficulty` (same
  8. * direction as spelling2, and consistent with the min(...) caps below)
  9. * - falls back to any word when nothing exists at the user's level, and shows
  10. * a message instead of crashing when the word list is empty
  11. * - the scramble is never the original word (up to 10 reshuffles)
  12. * - multibyte-safe letter splitting, escaped output
  13. * - user escaped in SQL; hard-coded `e107.` database name replaced by MPREFIX
  14. * - only the data the checker needs is stored (server-side via TkAnswerStore)
  15. * - previous tkLastQuestion row removed before inserting the new one
  16. */
  17. class anagrams {
  18. private $user;
  19. /**
  20. * @param string $user User to supply the question for.
  21. */
  22. public function __construct($user) {
  23. $this->user = $user;
  24. }
  25. public function getanagram() {
  26. $year = max(1, (int) ceil($this->difficulty()));
  27. $question = $this->pickWord($year);
  28. if ($question === null) {
  29. $question = $this->pickWord(13); // nothing at this level: fall back to any word
  30. }
  31. if ($question === null) {
  32. e107::getRender()->tablerender('Anagrams', 'No anagram words are available yet.');
  33. return;
  34. }
  35. $questionHtml = $this->anagramsHtml($question['word']);
  36. $db = e107::getDb();
  37. $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
  38. $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
  39. e107::getRender()->tablerender('Anagrams', $questionHtml);
  40. }
  41. /**
  42. * Pick a word at or below $year, preferring words this user has answered
  43. * least in the last month.
  44. *
  45. * @return array|null Row with `word`, or null when none found.
  46. */
  47. private function pickWord($year) {
  48. $db = e107::getDb();
  49. $row = $db->retrieve(
  50. "SELECT `word`, IFNULL(`cnt`, 0) AS Ncnt FROM
  51. (SELECT `word` FROM `" . MPREFIX . "tk_wordList`
  52. WHERE `year` <= " . (int) $year . " AND `usage` != '' AND CHAR_LENGTH(`word`) > 3) AS wList
  53. LEFT JOIN
  54. (SELECT `answer`, COUNT(`answer`) AS `cnt` FROM `" . MPREFIX . "tkReport`
  55. WHERE `answereddatestamp` > UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
  56. AND `username` = '" . $db->escape($this->user) . "'
  57. AND `subject` = 'anagrams' GROUP BY `answer`) AS Ans
  58. ON wList.`word` = Ans.`answer`
  59. ORDER BY IFNULL(`cnt`, 0), RAND() LIMIT 1"
  60. );
  61. return (is_array($row) && !empty($row['word'])) ? $row : null;
  62. }
  63. /**
  64. * Build the anagram form for $word. The expected answer is stored
  65. * server-side; the form only carries a token.
  66. */
  67. private function anagramsHtml($word) {
  68. $letters = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY);
  69. $shuffled = $letters;
  70. for ($i = 0; $i < 10; $i++) {
  71. shuffle($shuffled);
  72. if ($shuffled !== $letters) {
  73. break;
  74. }
  75. }
  76. $token = TkAnswerStore::issue($this->user, ['answer1' => $word, 'question' => $shuffled, 'type' => 'anagrams']);
  77. $frm = e107::getForm();
  78. $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
  79. $fString .= $frm->label('<h4>Anagrams</h4>') . "</br>\n";
  80. $fString .= $frm->label('Unjumble the letters to find the word.') . "\n";
  81. $fString .= '</br>';
  82. foreach ($shuffled as $letter) {
  83. $fString .= $frm->label(htmlspecialchars($letter) . " &nbsp; ") . "\n";
  84. }
  85. $fString .= '</br></br>';
  86. $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
  87. $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
  88. $fString .= $frm->hidden('subject', 'anagrams', ['id' => 'subject']);
  89. $fString .= '</br>' . "\n";
  90. $fString .= $frm->button('Answer', 'Check answer') . "\n";
  91. $fString .= $frm->close();
  92. $fString .= '</br></br>';
  93. return $fString;
  94. }
  95. /**
  96. * Difficulty (word year level 1-13) from the last month of answers.
  97. * Capped while the user has few answers.
  98. *
  99. * @return float
  100. */
  101. private function difficulty() {
  102. $db = e107::getDb();
  103. $stats = $db->retrieve(
  104. "SELECT (SUM(correct)/COUNT(correct)*13) AS percent, COUNT(correct) AS count
  105. FROM `" . MPREFIX . "tkReport`
  106. WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
  107. AND `username` = '" . $db->escape($this->user) . "' AND `subject` = 'anagrams'"
  108. );
  109. if (!is_array($stats)) {
  110. return 1;
  111. }
  112. $count = (int) $stats['count'];
  113. $percent = (float) $stats['percent'];
  114. if ($count >= 30) {
  115. return $percent;
  116. } elseif ($count >= 25) {
  117. return min($percent, 7);
  118. } elseif ($count >= 20) {
  119. return min($percent, 5);
  120. } elseif ($count >= 10) {
  121. return min($percent, 3);
  122. }
  123. return 1;
  124. }
  125. }