= difficulty`, i.e. beginners got every word * and strong players only the hardest; now `year <= difficulty` (same * direction as spelling2, and consistent with the min(...) caps below) * - falls back to any word when nothing exists at the user's level, and shows * a message instead of crashing when the word list is empty * - the scramble is never the original word (up to 10 reshuffles) * - multibyte-safe letter splitting, escaped output * - user escaped in SQL; hard-coded `e107.` database name replaced by MPREFIX * - only the data the checker needs is stored (server-side via TkAnswerStore) * - previous tkLastQuestion row removed before inserting the new one */ class anagrams { private $user; /** * @param string $user User to supply the question for. */ public function __construct($user) { $this->user = $user; } public function getanagram() { $year = max(1, (int) ceil($this->difficulty())); $question = $this->pickWord($year); if ($question === null) { $question = $this->pickWord(13); // nothing at this level: fall back to any word } if ($question === null) { e107::getRender()->tablerender('Anagrams', 'No anagram words are available yet.'); return; } $questionHtml = $this->anagramsHtml($question['word']); $db = e107::getDb(); $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'"); $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]); e107::getRender()->tablerender('Anagrams', $questionHtml); } /** * Pick a word at or below $year, preferring words this user has answered * least in the last month. * * @return array|null Row with `word`, or null when none found. */ private function pickWord($year) { $db = e107::getDb(); $row = $db->retrieve( "SELECT `word`, IFNULL(`cnt`, 0) AS Ncnt FROM (SELECT `word` FROM `" . MPREFIX . "tk_wordList` WHERE `year` <= " . (int) $year . " AND `usage` != '' AND CHAR_LENGTH(`word`) > 3) AS wList LEFT JOIN (SELECT `answer`, COUNT(`answer`) AS `cnt` FROM `" . MPREFIX . "tkReport` WHERE `answereddatestamp` > UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH) AND `username` = '" . $db->escape($this->user) . "' AND `subject` = 'anagrams' GROUP BY `answer`) AS Ans ON wList.`word` = Ans.`answer` ORDER BY IFNULL(`cnt`, 0), RAND() LIMIT 1" ); return (is_array($row) && !empty($row['word'])) ? $row : null; } /** * Build the anagram form for $word. The expected answer is stored * server-side; the form only carries a token. */ private function anagramsHtml($word) { $letters = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY); $shuffled = $letters; for ($i = 0; $i < 10; $i++) { shuffle($shuffled); if ($shuffled !== $letters) { break; } } $token = TkAnswerStore::issue($this->user, ['answer1' => $word, 'question' => $shuffled, 'type' => 'anagrams']); $frm = e107::getForm(); $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']); $fString .= $frm->label('

Anagrams

') . "
\n"; $fString .= $frm->label('Unjumble the letters to find the word.') . "\n"; $fString .= '
'; foreach ($shuffled as $letter) { $fString .= $frm->label(htmlspecialchars($letter) . "   ") . "\n"; } $fString .= '

'; $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', 'anagrams', ['id' => 'subject']); $fString .= '
' . "\n"; $fString .= $frm->button('Answer', 'Check answer') . "\n"; $fString .= $frm->close(); $fString .= '

'; return $fString; } /** * Difficulty (word year level 1-13) from the last month of answers. * Capped while the user has few answers. * * @return float */ private function difficulty() { $db = e107::getDb(); $stats = $db->retrieve( "SELECT (SUM(correct)/COUNT(correct)*13) 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` = 'anagrams'" ); if (!is_array($stats)) { return 1; } $count = (int) $stats['count']; $percent = (float) $stats['percent']; if ($count >= 30) { return $percent; } elseif ($count >= 25) { return min($percent, 7); } elseif ($count >= 20) { return min($percent, 5); } elseif ($count >= 10) { return min($percent, 3); } return 1; } }