PageSourceSearch

https://zhongalbany.github.io/guess-flags/script.js

js zhongalbany.github.io collected 2026-10-03 10:14:47 UTC 15,609 bytes, 483 lines download raw bytes

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.