.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 { const SUBJECT = 'flags'; private $user; private $level = 0; // 0-100: how much of the country list to use /** * @param string $user User to supply the question for. */ public function __construct($user) { $this->user = $user; } /** * 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); } /** * 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. */ public function getProblem() { $this->level = $this->difficulty(); $country = $this->pickCountry($this->flagFiles()); if ($country === null) { return null; } return [ 'answer1' => $country['country'], 'question' => 'Name this flag (' . $country['iso'] . ')', 'type' => self::SUBJECT, 'flag' => $country['file'], 'iso' => $country['iso'], ]; } /** * How much of the country list to use, 0-100: last month's accuracy, scaled * up over the first 30 answers. * * @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 . "'" ); 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('

Flags

') . "
\n"; $fString .= $frm->label('Which country has this flag? Type its name.') . "\n"; $fString .= '
'; // flag.php sends the flag for this student's open question (no file name, so no clue in the page). $fString .= '

A flag

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

'; return $fString; } }