|
@@ -1,101 +1,221 @@
|
|
|
<?php
|
|
<?php
|
|
|
|
|
|
|
|
-/*
|
|
|
|
|
- * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
|
|
|
|
|
- * Click nbfs://nbhost/SystemFileSystem/Templates/Scripting/EmptyPHP.php to edit this template
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Flags ("flags"): show a flag, the student types the country's name.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Replaces the placeholder that was a copy of the anagram generator.
|
|
|
|
|
+ *
|
|
|
|
|
+ * - The flags come from resources/flags/<ISO alpha-3 code>.svg (or .png). A
|
|
|
|
|
+ * country is only asked if it is in tk_countries with its isoCode filled
|
|
|
|
|
+ * in AND there is a flag file for that code.
|
|
|
|
|
+ * - The flag is not linked by file name: the page shows flag.php, which sends
|
|
|
|
|
+ * the flag for the student's open question. A name like AUS.svg in the page
|
|
|
|
|
+ * would give the answer away.
|
|
|
|
|
+ * - Easy countries first: tk_countries.difficulty (1 = easy ... 10 = hard)
|
|
|
|
|
+ * decides the order. At level 0 a student gets the easiest 15% of the
|
|
|
|
|
+ * countries, and the pool grows to all of them as their accuracy grows
|
|
|
|
|
+ * (scaled up over their first 30 answers). Within the same difficulty the
|
|
|
|
|
+ * order is shuffled differently for each student.
|
|
|
|
|
+ * - Countries the student has been asked least this month come first.
|
|
|
|
|
+ * - Names are decoded from e107's HTML entities, and a code that has two rows
|
|
|
|
|
+ * (Bosnia and Herzegovina) is asked once.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The expected answer is stored server-side (TkAnswerStore); the checker
|
|
|
|
|
+ * (answerChecker2::checkFlags) accepts alternative names such as "USA" or
|
|
|
|
|
+ * "Holland" through CountryNames.
|
|
|
*/
|
|
*/
|
|
|
|
|
+class flags {
|
|
|
|
|
|
|
|
-class continents
|
|
|
|
|
-{
|
|
|
|
|
|
|
+ const SUBJECT = 'flags';
|
|
|
|
|
|
|
|
- private $user;
|
|
|
|
|
|
|
+ private $user;
|
|
|
|
|
+ private $level = 0; // 0-100: how much of the country list to use
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Setup new spelling engine
|
|
|
|
|
- *
|
|
|
|
|
- * @param bool $user user to supply question for.
|
|
|
|
|
|
|
+ * @param string $user User to supply the question for.
|
|
|
*/
|
|
*/
|
|
|
public function __construct($user) {
|
|
public function __construct($user) {
|
|
|
$this->user = $user;
|
|
$this->user = $user;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public function getgeography() {
|
|
|
|
|
-
|
|
|
|
|
- $difficulty = $this->difficulty();
|
|
|
|
|
-
|
|
|
|
|
- $question = e107::getDb()->retrieve("SELECT `word`,`usage`, IFNULL(`cnt`,'0') AS Ncnt FROM
|
|
|
|
|
- (SELECT `word`,`usage` FROM e107.e107_tk_wordList WHERE `year` >= '".ceil($difficulty)."' AND `usage` != '' AND CHAR_LENGTH(`word`)>3) AS wList LEFT JOIN
|
|
|
|
|
- (SELECT `answer`,COUNT(`answer`)AS `cnt` FROM e107.e107_tkReport
|
|
|
|
|
- WHERE `answereddatestamp` > unix_timestamp((NOW()) - INTERVAL 1 MONTH) AND `username` = '".$this->user."'
|
|
|
|
|
- AND `subject` = 'anagrams' GROUP BY `answer` ORDER BY COUNT(`answer`)) AS Ans
|
|
|
|
|
- ON wList.`word` = Ans.`answer` ORDER BY IFNULL(`cnt`,'0'), RAND() LIMIT 1;");
|
|
|
|
|
-
|
|
|
|
|
- $questionHtml = $this->anagramsHtml($question);
|
|
|
|
|
-
|
|
|
|
|
- e107::getDb()->insert('tkLastQuestion', ['user'=>$this->user,'LastQuestionHtml'=>base64_encode($questionHtml)]);
|
|
|
|
|
-
|
|
|
|
|
- e107::getRender()->tablerender('Anagrams', $questionHtml);
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Make a question, show it, and remember it as the student's open question.
|
|
|
|
|
+ */
|
|
|
|
|
+ public function getFlag() {
|
|
|
|
|
+ $problem = $this->getProblem();
|
|
|
|
|
+
|
|
|
|
|
+ if ($problem === null) {
|
|
|
|
|
+ e107::getRender()->tablerender('Flags', 'There are no flags to ask about yet. Please refresh the page.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $questionHtml = $this->htmlify($problem);
|
|
|
|
|
+
|
|
|
|
|
+ $db = e107::getDb();
|
|
|
|
|
+ $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
|
|
|
|
|
+ $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
|
|
|
|
|
+
|
|
|
|
|
+ e107::getRender()->tablerender('Flags', $questionHtml);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Create a spelling form using the supplied question.
|
|
|
|
|
- * And write it into "$this->returnQuestion".
|
|
|
|
|
- *
|
|
|
|
|
- * @param array() $data Question data to be used.
|
|
|
|
|
|
|
+ * Choose a flag without showing it.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return array|null ['answer1' => country name, 'question' => string, 'type' => 'flags',
|
|
|
|
|
+ * 'flag' => file name, 'iso' => code], or null when no flag can be asked.
|
|
|
*/
|
|
*/
|
|
|
- private function anagramsHtml($data) {
|
|
|
|
|
-
|
|
|
|
|
- $frm = e107::getForm();
|
|
|
|
|
|
|
+ public function getProblem() {
|
|
|
|
|
+ $this->level = $this->difficulty();
|
|
|
|
|
|
|
|
- $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
|
|
|
|
|
- $fString .= $frm->label('<h4>Anagrams</h4>') . "</br>\n";
|
|
|
|
|
- $fString .= $frm->label('Unjumble the letters to find the word.') . "\n";
|
|
|
|
|
- $fString .= '</br>';
|
|
|
|
|
-
|
|
|
|
|
- $data['answer1'] = $data['word'];
|
|
|
|
|
- $data['question'] = str_split($data['word']);
|
|
|
|
|
- shuffle($data['question']);
|
|
|
|
|
-
|
|
|
|
|
- foreach ($data['question'] as $seq) {
|
|
|
|
|
- $fString .= $frm->label($seq . " ") . "\n";
|
|
|
|
|
|
|
+ $country = $this->pickCountry($this->flagFiles());
|
|
|
|
|
+ if ($country === null) {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- $fString .= '</br></br>';
|
|
|
|
|
- $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
|
|
|
|
|
- $fString .= $frm->hidden('checkerData', base64_encode(json_encode($data)), ['id' => 'checkerData']);
|
|
|
|
|
- $fString .= $frm->hidden('subject', 'anagrams', ['id' => 'subject']);
|
|
|
|
|
- $fString .= '</br>' . "\n";
|
|
|
|
|
- $fString .= e107::getForm()->button('Answer', 'Check answer') . "\n";
|
|
|
|
|
- $fString .= e107::getForm()->close();
|
|
|
|
|
- $fString .= '</br></br>';
|
|
|
|
|
-
|
|
|
|
|
- return $fString;
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'answer1' => $country['country'],
|
|
|
|
|
+ 'question' => 'Name this flag (' . $country['iso'] . ')',
|
|
|
|
|
+ 'type' => self::SUBJECT,
|
|
|
|
|
+ 'flag' => $country['file'],
|
|
|
|
|
+ 'iso' => $country['iso'],
|
|
|
|
|
+ ];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * Return the difficulty based on previously answered questions.
|
|
|
|
|
|
|
+ * How much of the country list to use, 0-100: last month's accuracy, scaled
|
|
|
|
|
+ * up over the first 30 answers.
|
|
|
*
|
|
*
|
|
|
- * @return array['percent','count'] Difficulty 1-13, Number of questions answered.
|
|
|
|
|
|
|
+ * @return float
|
|
|
*/
|
|
*/
|
|
|
private function difficulty() {
|
|
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 . "'"
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- $stats = e107::getDb()->retrieve("SELECT (SUM(correct)/COUNT(correct)*13) as percent, COUNT(correct) as count
|
|
|
|
|
- FROM e107.`e107_tkReport` WHERE
|
|
|
|
|
- answereddatestamp >= unix_timestamp((NOW()) - INTERVAL 1 MONTH)
|
|
|
|
|
- AND `username` = '$this->user' AND `subject` = 'continents';"
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- if($stats["count"]>=30) {
|
|
|
|
|
- return $stats["percent"];
|
|
|
|
|
- } elseif($stats["count"]>=25) {
|
|
|
|
|
- return min($stats["percent"],7);
|
|
|
|
|
- } elseif($stats["count"]>=20) {
|
|
|
|
|
- return min($stats["percent"],5);
|
|
|
|
|
- } elseif($stats["count"]>=10) {
|
|
|
|
|
- return min($stats["percent"],3);
|
|
|
|
|
- } else {
|
|
|
|
|
- return 1;
|
|
|
|
|
|
|
+ if (!is_array($stats) || (int) $stats['count'] === 0) {
|
|
|
|
|
+ return 0.0;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ $sample = min(1.0, ((int) $stats['count']) / 30);
|
|
|
|
|
+ return max(0.0, min(100.0, (float) $stats['percent'])) * $sample;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The flag files that exist: ['AUS' => 'AUS.svg', ...]
|
|
|
|
|
+ */
|
|
|
|
|
+ private function flagFiles() {
|
|
|
|
|
+ $dir = dirname(__DIR__, 2) . '/resources/flags';
|
|
|
|
|
+ $files = [];
|
|
|
|
|
+
|
|
|
|
|
+ foreach (['svg', 'png'] as $extension) {
|
|
|
|
|
+ $found = glob($dir . '/*.' . $extension);
|
|
|
|
|
+ foreach (($found ? $found : []) as $path) {
|
|
|
|
|
+ $code = strtoupper(pathinfo($path, PATHINFO_FILENAME));
|
|
|
|
|
+ if (preg_match('/^[A-Z]{3}$/', $code) && !isset($files[$code])) {
|
|
|
|
|
+ $files[$code] = basename($path);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $files;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param array $files ['AUS' => 'AUS.svg', ...]
|
|
|
|
|
+ * @return array|null ['country', 'iso', 'file', 'difficulty']
|
|
|
|
|
+ */
|
|
|
|
|
+ private function pickCountry(array $files) {
|
|
|
|
|
+ $db = e107::getDb();
|
|
|
|
|
+
|
|
|
|
|
+ $seen = [];
|
|
|
|
|
+ // Countries that have both an ISO code and a flag file, one row per code
|
|
|
|
|
+ // (the first by id: the table has two rows, Bosnia and Herzegovina, for BIH).
|
|
|
|
|
+ $countries = [];
|
|
|
|
|
+ $db->gen("SELECT `country`, `isoCode`, `difficulty` FROM `" . MPREFIX . "tk_countries` WHERE `isoCode` != '' ORDER BY `id`");
|
|
|
|
|
+ while ($row = $db->fetch()) {
|
|
|
|
|
+ $code = strtoupper(trim($row['isoCode']));
|
|
|
|
|
+ // e107 stores an apostrophe as ' ("Cote d'Ivoire"): show and compare the real name.
|
|
|
|
|
+ $name = trim(html_entity_decode($row['country'], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
|
|
|
|
+ if ($name !== '' && isset($files[$code]) && !isset($seen[$code])) {
|
|
|
|
|
+ $seen[$code] = true;
|
|
|
|
|
+ $countries[] = [
|
|
|
|
|
+ 'country' => $name,
|
|
|
|
|
+ 'iso' => $code,
|
|
|
|
|
+ 'file' => $files[$code],
|
|
|
|
|
+ 'difficulty' => (int) $row['difficulty'],
|
|
|
|
|
+ 'order' => crc32($this->user . '|' . $code),
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (count($countries) < 4) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // How often each has been asked this month.
|
|
|
|
|
+ $asked = [];
|
|
|
|
|
+ $db->gen("SELECT `answer`, COUNT(*) AS cnt FROM `" . MPREFIX . "tkReport`
|
|
|
|
|
+ WHERE `username` = '" . $db->escape($this->user) . "' AND `subject` = '" . self::SUBJECT . "'
|
|
|
|
|
+ AND `answereddatestamp` >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH) GROUP BY `answer`");
|
|
|
|
|
+ while ($row = $db->fetch()) {
|
|
|
|
|
+ $asked[$row['answer']] = (int) $row['cnt'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Easiest first; the same difficulty in an order of this student's own.
|
|
|
|
|
+ usort($countries, function ($a, $b) {
|
|
|
|
|
+ if ($a['difficulty'] !== $b['difficulty']) {
|
|
|
|
|
+ return $a['difficulty'] <=> $b['difficulty'];
|
|
|
|
|
+ }
|
|
|
|
|
+ return $a['order'] <=> $b['order'];
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ $total = count($countries);
|
|
|
|
|
+ $size = (int) ceil($total * (0.15 + 0.85 * $this->level / 100));
|
|
|
|
|
+ $size = max(min($total, 8), min($total, $size));
|
|
|
|
|
+ $pool = array_slice($countries, 0, $size);
|
|
|
|
|
+
|
|
|
|
|
+ // The least-asked countries in the pool, one at random.
|
|
|
|
|
+ $fewest = null;
|
|
|
|
|
+ foreach ($pool as $country) {
|
|
|
|
|
+ $count = isset($asked[$country['country']]) ? $asked[$country['country']] : 0;
|
|
|
|
|
+ if ($fewest === null || $count < $fewest) {
|
|
|
|
|
+ $fewest = $count;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $candidates = [];
|
|
|
|
|
+ foreach ($pool as $country) {
|
|
|
|
|
+ $count = isset($asked[$country['country']]) ? $asked[$country['country']] : 0;
|
|
|
|
|
+ if ($count === $fewest) {
|
|
|
|
|
+ $candidates[] = $country;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $candidates[array_rand($candidates)];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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>Flags</h4>') . "</br>\n";
|
|
|
|
|
+ $fString .= $frm->label('Which country has this flag? Type its name.') . "\n";
|
|
|
|
|
+ $fString .= '</br>';
|
|
|
|
|
+ // flag.php sends the flag for this student's open question (no file name, so no clue in the page).
|
|
|
|
|
+ $fString .= '<p><img src="flag.php?r=' . bin2hex(random_bytes(4)) . '" alt="A flag" style="width:360px;max-width:100%;height:auto;border:1px solid #999;"></p>';
|
|
|
|
|
+ $fString .= $frm->text('a1', '', 60, ['placeholder' => 'Write the country 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;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|