1const DATA_URL = "data/countries.json"; 2const APP_VERSION = "v1.0.3"; 3const STORAGE_KEYS = { 4 stats: "guessFlags.stats", 5 mistakes: "guessFlags.mistakes", 6 settings: "guessFlags.settings", 7 daily: "guessFlags.daily" 8}; 9 10const screens = { 11 home: document.getElementById("homeScreen"), 12 game: document.getElementById("gameScreen"), 13 stats: document.getElementById("statsScreen"), 14 mistakes: document.getElementById("mistakesScreen") 15}; 16 17const regionSelect = document.getElementById("regionSelect"); 18const chineseModeToggle = document.getElementById("chineseModeToggle"); 19const dailyButton = document.getElementById("dailyButton"); 20const dailyStatus = document.getElementById("dailyStatus"); 21const versionLabel = document.getElementById("versionLabel"); 22const flagDisplay = document.getElementById("flagDisplay"); 23const answerForm = document.getElementById("answerForm"); 24const answerInput = document.getElementById("answerInput"); 25const suggestions = document.getElementById("suggestions"); 26const submitButton = document.getElementById("submitButton"); 27const nextButton = document.getElementById("nextButton"); 28const studyPanel = document.getElementById("studyPanel"); 29const resultsPanel = document.getElementById("resultsPanel"); 30const resultText = document.getElementById("resultText"); 31const factCountry = document.getElementById("factCountry"); 32const factCapital = document.getElementById("factCapital"); 33const factContinent = document.getElementById("factContinent"); 34const finalScoreText = document.getElementById("finalScoreText"); 35const finalAccuracyText = document.getElementById("finalAccuracyText"); 36const scoreText = document.getElementById("scoreText"); 37const gameModeLabel = document.getElementById("gameModeLabel"); 38const statsContent = document.getElementById("statsContent"); 39const mistakesContent = document.getElementById("mistakesContent"); 40const practiceMistakesButton = document.getElementById("practiceMistakesButton"); 41const clearMistakesButton = document.getElementById("clearMistakesButton"); 42 43let countries = []; 44let currentPool = []; 45let currentCountry = null; 46let gameMode = "normal"; 47let score = { correct: 0, attempted: 0 }; 48let hasAnsweredCurrentFlag = false; 49let dailyIndex = 0; 50let questionNumber = 0; 51 52const emptyStats = { 53 attempts: 0, 54 correct: 0, 55 currentStreak: 0, 56 highestStreak: 0, 57 byContinent: {} 58}; 59 60let stats = loadFromStorage(STORAGE_KEYS.stats, emptyStats); 61let mistakes = loadFromStorage(STORAGE_KEYS.mistakes, {}); 62let settings = loadFromStorage(STORAGE_KEYS.settings, { chineseMode: false }); 63let dailyScores = loadFromStorage(STORAGE_KEYS.daily, {}); 64let chineseMode = Boolean(settings.chineseMode); 65chineseModeToggle.checked = chineseMode; 66versionLabel.textContent = APP_VERSION; 67 68function isChineseModeOn() { 69 return chineseModeToggle.checked; 70} 71 72function loadFromStorage(key, fallback) { 73 try { 74 return JSON.parse(localStorage.getItem(key)) || JSON.parse(JSON.stringify(fallback)); 75 } catch (error) { 76 return JSON.parse(JSON.stringify(fallback)); 77 } 78} 79 80function saveStats() { 81 localStorage.setItem(STORAGE_KEYS.stats, JSON.stringify(stats)); 82} 83 84function saveMistakes() { 85 localStorage.setItem(STORAGE_KEYS.mistakes, JSON.stringify(mistakes)); 86} 87 88function saveSettings() { 89 localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings)); 90} 91 92function saveDailyScores() { 93 localStorage.setItem(STORAGE_KEYS.daily, JSON.stringify(dailyScores)); 94} 95 96function normalizeAnswer(value) { 97 return value 98 .toLowerCase() 99 .replace(/&/g, "and") 100 .replace(/[^a-z0-9\u00c0-\u024f\u3400-\u9fff]+/g, " ") 101 .trim(); 102} 103
104function acceptedAnswers(country) { 105 const answers = [country.name].concat(country.aliases || []); 106 if (isChineseModeOn() && country.zh) answers.push(country.zh); 107 return answers.map(normalizeAnswer); 108} 109 110function isCorrectAnswer(value, country) { 111 return acceptedAnswers(country).includes(normalizeAnswer(value)); 112} 113 114function showScreen(name) { 115 Object.keys(screens).forEach((screenName) => { 116 screens[screenName].classList.toggle("screen-active", screenName === name); 117 }); 118 119 if (name === "home") renderDailyStatus(); 120 if (name === "stats") renderStats(); 121 if (name === "mistakes") renderMistakes(); 122} 123 124function countriesForSelectedRegion() { 125 const region = regionSelect.value; 126 if (region === "All") return countries.slice(); 127 return countries.filter((country) => country.continent === region); 128} 129 130function shuffledCountries(list) { 131 const shuffled = list.slice(); 132 for (let i = shuffled.length - 1; i > 0; i -= 1) { 133 const j = Math.floor(Math.random() * (i + 1)); 134 const temp = shuffled[i]; 135 shuffled[i] = shuffled[j]; 136 shuffled[j] = temp; 137 } 138 return shuffled; 139} 140 141function startGame(mode) { 142 gameMode = mode; 143 const mistakeIds = Object.keys(mistakes); 144 dailyIndex = 0; 145 questionNumber = 0; 146 147 if (mode === "daily") { 148 currentPool = dailyCountries(); 149 } else { 150 currentPool = mode === "mistakes" 151 ? countries.filter((country) => mistakeIds.includes(country.code)) 152 : shuffledCountries(countriesForSelectedRegion()); 153 } 154 155 if (!currentPool.length) { 156 showScreen("mistakes"); 157 return; 158 } 159 160 score = { correct: 0, attempted: 0 }; 161 if (mode === "daily") { 162 gameModeLabel.textContent = "Daily Challenge"; 163 } else { 164 gameModeLabel.textContent = mode === "mistakes" ? "Mistakes practice" : `${regionSelect.value} countries`; 165 } 166 showScreen("game"); 167 nextQuestion(); 168} 169 170function nextQuestion() { 171 if (gameMode === "daily" && dailyIndex >= currentPool.length) { 172 finishDailyChallenge(); 173 return; 174 } 175 176 if (gameMode === "normal" && questionNumber >= currentPool.length) { 177 showNormalResults(); 178 return; 179 } 180 181 if (gameMode === "daily") { 182 currentCountry = currentPool[dailyIndex++]; 183 } else if (gameMode === "normal") { 184 currentCountry = currentPool[questionNumber]; 185 } else { 186 currentCountry = currentPool[Math.floor(Math.random() * currentPool.length)]; 187 } 188 189 questionNumber += 1; 190 hasAnsweredCurrentFlag = false; 191 flagDisplay.textContent = currentCountry.flag; 192 answerInput.value = ""; 193 answerInput.disabled = false; 194 submitButton.disabled = false; 195 submitButton.hidden = false; 196 suggestions.innerHTML = ""; 197 studyPanel.hidden = true; 198 resultsPanel.hidden = true; 199 nextButton.textContent = "Next"; 200 updateScore(); 201 window.setTimeout(() => answerInput.focus(), 80); 202} 203 204function updateScore() { 205 const totalQuestions = currentPool.length || 0; 206 const visibleQuestion = Math.min(questionNumber || 1, totalQuestions || 1); 207 scoreText.innerHTML = ` 208 <span class="score-line">Question ${visibleQuestion} / ${totalQuestions}</span> 209 <span class="score-line">Correct ${score.correct} / ${score.attempted}</span> 210 `; 211} 212 213function submitAnswer(value) { 214 if (!currentCountry || hasAnsweredCurrentFlag) return; 215 216 const correct = isCorrectAnswer(value, currentCountry); 217 hasAnsweredCurrentFlag = true; 218 score.attempted += 1; 219 stats.attempts += 1; 220 221 if (!stats.byContinent[currentCountry.continent]) { 222 stats.byContinent[currentCountry.continent] = { attempts: 0, correct: 0 }; 223 } 224 stats.byContinent[currentCountry.continent].attempts += 1; 225 226 if (correct) { 227 score.correct += 1; 228 stats.correct += 1; 229 stats.currentStreak += 1; 230 stats.highestStreak = Math.max(stats.highestStreak, stats.currentStreak); 231 stats.byContinent[currentCountry.continent].correct += 1; 232 } else { 233 stats.currentStreak = 0; 234 mistakes[currentCountry.code] = (mistakes[currentCountry.code] || 0) + 1; 235 } 236 237 saveStats(); 238 saveMistakes(); 239 showStudyMode(correct); 240} 241 242function showStudyMode(correct) { 243 resultText.textContent = correct ? "Correct!" : "Not quite. Study this one."; 244 resultText.className = `result-text ${correct ? "correct" : "wrong"}`; 245 factCountry.textContent = isChineseModeOn() && currentCountry.zh 246 ? `${currentCountry.zh} (${currentCountry.name})` 247 : currentCountry.name; 248 factCapital.textContent = currentCountry.capital; 249 factContinent.textContent = currentCountry.continent; 250 answerInput.disabled = true; 251 submitButton.disabled = true; 252 submitButton.hidden = true; 253 suggestions.innerHTML = ""; 254 studyPanel.hidden = false;
255 if (gameMode === "daily" && score.attempted >= currentPool.length) { 256 saveDailyResult(); 257 nextButton.textContent = "Finish"; 258 } 259 if (gameMode === "normal" && score.attempted >= currentPool.length) { 260 nextButton.textContent = "See Results"; 261 } 262 updateScore(); 263} 264 265function showNormalResults() { 266 currentCountry = null; 267 hasAnsweredCurrentFlag = true; 268 flagDisplay.textContent = "ð"; 269 answerInput.value = ""; 270 answerInput.disabled = true; 271 submitButton.disabled = true; 272 submitButton.hidden = true; 273 suggestions.innerHTML = ""; 274 studyPanel.hidden = true; 275 resultsPanel.hidden = false; 276 finalScoreText.textContent = `${score.correct} / ${score.attempted}`; 277 finalAccuracyText.textContent = accuracy(score.correct, score.attempted); 278 updateScore(); 279} 280 281function renderSuggestions() { 282 const query = normalizeAnswer(answerInput.value); 283 suggestions.innerHTML = ""; 284 if (!query) return; 285 286 suggestionPool() 287 .map((country) => ({ country, rank: suggestionRank(country, query) })) 288 .filter((match) => match.rank < 99) 289 .sort((a, b) => a.rank - b.rank || a.country.name.localeCompare(b.country.name)) 290 .slice(0, 10) 291 .forEach((match) => { 292 const country = match.country; 293 const button = document.createElement("button"); 294 button.className = "suggestion"; 295 button.type = "button"; 296 button.setAttribute("role", "option"); 297 button.textContent = displayCountryName(country); 298 button.addEventListener("click", () => submitAnswer(isChineseModeOn() && country.zh ? country.zh : country.name)); 299 suggestions.appendChild(button); 300 }); 301} 302 303function suggestionNames(country) { 304 const names = [country.name].concat(country.aliases || []); 305 if (country.zh) names.push(country.zh); 306 return names; 307} 308 309function suggestionRank(country, query) { 310 const names = suggestionNames(country).map(normalizeAnswer); 311 if (names.some((name) => name === query)) return 0; 312 if (names.some((name) => name.startsWith(query))) return 1; 313 if (names.some((name) => name.split(" ").some((part) => part.startsWith(query)))) return 2; 314 if (names.some((name) => name.includes(query))) return 3; 315 return 99; 316} 317 318function suggestionPool() { 319 if (screens.game.classList.contains("screen-active") && currentPool.length) { 320 return currentPool; 321 } 322 return countriesForSelectedRegion(); 323} 324 325function displayCountryName(country) { 326 if (isChineseModeOn() && country.zh) return `${country.zh} (${country.name})`; 327 return country.name; 328} 329 330function todayKey() { 331 const now = new Date();
332 const month = String(now.getMonth() + 1).padStart(2, "0"); 333 const day = String(now.getDate()).padStart(2, "0"); 334 return `${now.getFullYear()}-${month}-${day}`; 335} 336 337function seededNumber(seedText) { 338 let seed = 0; 339 for (let i = 0; i < seedText.length; i += 1) { 340 seed = (seed * 31 + seedText.charCodeAt(i)) >>> 0; 341 } 342 return seed || 1; 343} 344 345function randomFromSeed(seed) { 346 let value = seed; 347 return () => { 348 value = (value * 1664525 + 1013904223) >>> 0; 349 return value / 4294967296; 350 }; 351} 352 353function dailyCountries() { 354 const random = randomFromSeed(seededNumber(todayKey())); 355 const list = countries.slice(); 356 for (let i = list.length - 1; i > 0; i -= 1) { 357 const j = Math.floor(random() * (i + 1)); 358 const temp = list[i]; 359 list[i] = list[j]; 360 list[j] = temp; 361 } 362 return list.slice(0, 20); 363} 364 365function saveDailyResult() { 366 dailyScores[todayKey()] = { 367 correct: score.correct, 368 attempted: score.attempted, 369 completed: true 370 }; 371 saveDailyScores(); 372 renderDailyStatus(); 373} 374 375function finishDailyChallenge() { 376 saveDailyResult(); 377 showScreen("home"); 378} 379 380function renderDailyStatus() { 381 const result = dailyScores[todayKey()]; 382 if (!result || !result.completed) { 383 dailyStatus.hidden = true; 384 dailyStatus.textContent = ""; 385 dailyButton.textContent = "Daily Challenge"; 386 return; 387 } 388 389 dailyStatus.hidden = false; 390 dailyStatus.textContent = `Today's Daily Challenge: ${result.correct} / ${result.attempted}. Replay anytime.`; 391 dailyButton.textContent = "Replay Daily Challenge"; 392} 393 394function accuracy(correct, attempts) { 395 if (!attempts) return "0%"; 396 return `${Math.round((correct / attempts) * 100)}%`; 397} 398 399function panelItem(label, value) { 400 const item = document.createElement("div"); 401 item.className = "panel-item"; 402 item.innerHTML = `<span class="panel-label">${label}</span><span class="panel-value">${value}</span>`; 403 return item; 404} 405
406function renderStats() { 407 statsContent.innerHTML = ""; 408 statsContent.appendChild(panelItem("Total accuracy", accuracy(stats.correct, stats.attempts))); 409 statsContent.appendChild(panelItem("Highest streak", stats.highestStreak)); 410 411 ["Asia", "Europe", "Africa", "North America", "South America", "Oceania"].forEach((continent) => { 412 const row = stats.byContinent[continent] || { attempts: 0, correct: 0 }; 413 statsContent.appendChild(panelItem(continent, accuracy(row.correct, row.attempts))); 414 }); 415} 416 417function renderMistakes() { 418 mistakesContent.innerHTML = ""; 419 const mistakeCountries = countries 420 .filter((country) => mistakes[country.code]) 421 .sort((a, b) => a.name.localeCompare(b.name)); 422 423 practiceMistakesButton.disabled = mistakeCountries.length === 0; 424 clearMistakesButton.disabled = mistakeCountries.length === 0; 425 426 if (!mistakeCountries.length) { 427 const empty = document.createElement("p"); 428 empty.className = "empty-state"; 429 empty.textContent = "No mistakes yet!"; 430 mistakesContent.appendChild(empty); 431 return; 432 } 433 434 mistakeCountries.forEach((country) => { 435 mistakesContent.appendChild(panelItem(`${country.flag} ${country.name}`, `${mistakes[country.code]} miss${mistakes[country.code] === 1 ? "" : "es"}`)); 436 }); 437} 438 439document.getElementById("startButton").addEventListener("click", () => startGame("normal")); 440dailyButton.addEventListener("click", () => startGame("daily")); 441document.getElementById("statsButton").addEventListener("click", () => showScreen("stats")); 442document.getElementById("mistakesButton").addEventListener("click", () => showScreen("mistakes")); 443document.getElementById("homeButton").addEventListener("click", () => showScreen("home")); 444document.querySelectorAll(".back-home").forEach((button) => { 445 button.addEventListener("click", () => showScreen("home")); 446}); 447 448answerForm.addEventListener("submit", (event) => { 449 event.preventDefault(); 450 submitAnswer(answerInput.value); 451}); 452 453answerInput.addEventListener("input", renderSuggestions); 454nextButton.addEventListener("click", nextQuestion); 455document.getElementById("playAgainButton").addEventListener("click", () => startGame("normal")); 456document.getElementById("resultsHomeButton").addEventListener("click", () => showScreen("home")); 457practiceMistakesButton.addEventListener("click", () => startGame("mistakes")); 458chineseModeToggle.addEventListener("change", () => { 459 chineseMode = chineseModeToggle.checked; 460 settings.chineseMode = chineseMode; 461 saveSettings(); 462}); 463clearMistakesButton.addEventListener("click", () => { 464 mistakes = {}; 465 saveMistakes(); 466 renderMistakes(); 467}); 468 469if ("serviceWorker" in navigator) { 470 window.addEventListener("load", () => { 471 navigator.serviceWorker.register("./service-worker.js", { scope: "./" }); 472 }); 473} 474 475fetch(DATA_URL) 476 .then((response) => response.json()) 477 .then((data) => { 478 countries = data; 479 renderDailyStatus(); 480 }) 481 .catch(() => { 482 document.querySelector(".hero p").textContent = "Country data could not load."; 483 });
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.