From 2576ffe4f76a92e523f56363754901923956beff Mon Sep 17 00:00:00 2001 From: Mathieu VIART Date: Mon, 2 Mar 2026 14:22:55 +0000 Subject: [PATCH] Minor bugs resolution - improved compatibility with mobile. --- api/server.js | 5 +++++ country.json | 4 ++-- js/daily.js | 26 +++++++++----------------- js/game-end.js | 14 +++++++++++++- js/game.js | 31 ++++++++++++++++++++++++++++--- js/seed.js | 6 ++++++ js/utils.js | 5 +++++ 7 files changed, 68 insertions(+), 23 deletions(-) diff --git a/api/server.js b/api/server.js index 685f2e2..f2edeb3 100644 --- a/api/server.js +++ b/api/server.js @@ -104,6 +104,11 @@ const server = http.createServer(async (req, res) => { } // GET /api/played + // NOTE: the endpoint simply remembers a hashed IP for the day. this is + // trivially circumventable by using a different IP/proxy or by clearing + // cookies. no server‑side score validation is performed either; the leaderboard + // is for fun only. if this service ever becomes competitive it should be + // hardened (rate‑limit, score sanity checks, CAPTCHAs, etc.). if (path === '/api/played' && req.method === 'GET') { const date = todayUTC(); const ipHash = hashIP(getIP(req)); diff --git a/country.json b/country.json index 99335ac..4c771a5 100644 --- a/country.json +++ b/country.json @@ -12802,7 +12802,7 @@ { "Rang": 158, "Pays": "Nagorno-Karabakh", - "Code ISO2": "N\/A", + "Code ISO2": "", "Indice GAI": 2.54 }, { @@ -12886,7 +12886,7 @@ { "Rang": 172, "Pays": "Somaliland", - "Code ISO2": "N\/A", + "Code ISO2": "", "Indice GAI": 1.59 }, { diff --git a/js/daily.js b/js/daily.js index 539a0c3..9825548 100644 --- a/js/daily.js +++ b/js/daily.js @@ -149,23 +149,15 @@ function _shuffle(arr, rng) { // ── Fin de partie Daily : intercepte showEndPanel ───────────────────────────── var _origShowEndPanel = null; -function _dailyShowEndPanel() { - // Appeler l'original - _origShowEndPanel(); - - if (!isDailyMode) return; - - // Cacher la combinaison optimale et la seed (anti-triche daily) - var optBlock = document.getElementById('opt-block'); - if (optBlock) optBlock.style.display = 'none'; - var seedBox = document.getElementById('seed-display'); - if (seedBox) seedBox.style.display = 'none'; - var copyBtns = document.querySelector('[id="btn-copy-seed"]'); - if (copyBtns) copyBtns.parentElement.style.display = 'none'; - - // Sauvegarder le score et ouvrir le modal submit - dailyScore = totalScore; - setTimeout(function() { openSubmitModal(); }, 600); +function skipDailySubmit() { + var overlay = document.getElementById('daily-submit-overlay'); + if (overlay) overlay.style.display = 'none'; + _setDailyPlayed(); + // leave isDailyMode true until after the leaderboard opens, just in case + setTimeout(function() { + isDailyMode = false; + openLeaderboard(); + }, 200); } // ── Modal submit ────────────────────────────────────────────────────────────── diff --git a/js/game-end.js b/js/game-end.js index 8ef2bab..337f2d0 100644 --- a/js/game-end.js +++ b/js/game-end.js @@ -227,13 +227,25 @@ function animateCounter(el, from, to, duration, onStep) { function showEndPanel() { document.getElementById('panel-end').classList.remove('hidden'); - // Recompute from gameLog to be bulletproof against any NaN accumulation + // Recompute from gameLog to be bulletproof against any NaN accumulation. + // + // WARNING / INVARIANT: entries logged for hint penalties carry a positive + // `points` value and are added to totalScore during play. we therefore + // _also_ include them in this recomputation; the computed score must match + // the running total exactly. if the hint logic ever changes (e.g. storing + // negative penalties, separating the entries, or removing the recompute + // entirely) this loop needs to be updated accordingly. leaving the code as + // it is protects us from the classic "score doubled after refactor" bug. var computedScore = 0; gameLog.forEach(function(e) { var v = Number(e.points); if (!isNaN(v)) computedScore += v; }); + if (computedScore !== totalScore) { + console.warn('score mismatch in showEndPanel', computedScore, totalScore); + } var safeScore = computedScore; + // overwrite global in case some consumer reads it later (daily, replay, ...) totalScore = safeScore; document.getElementById('final-score').textContent = safeScore; var tl = t('taglines'); diff --git a/js/game.js b/js/game.js index 788e66b..f37b38c 100644 --- a/js/game.js +++ b/js/game.js @@ -574,6 +574,12 @@ function showTurn() { // ─── REVERSE MODE TURN ──────────────────────────────────────────────────────── function showTurnReverse() { + // guard against malformed seed: we must check against the *categories* + // array rather than gameCountries (which is what showTurn checks). the + // two lengths normally match, but diverging lengths used to allow the game + // to continue one turn too many when there was a generation bug. + if (currentStep >= gameCategories.length) { endGame(); return; } + // make sure any open cat-tooltip from normal mode is shut, removing ghost flashes hideTooltip(); var cat = gameCategories[currentStep]; @@ -699,7 +705,9 @@ function handleCountryChoice(countryIdx) { function autoPick_reverse() { var firstFree = -1; for (var i = 0; i < gameCountries.length; i++) { - if (!reverseAssignments[i]) { firstFree = i; break; } + // only treat a slot as free if it hasn't been assigned at all; falsy + // category ids (0, "", etc.) should not trigger the fallback. + if (reverseAssignments[i] === undefined) { firstFree = i; break; } } if (firstFree === -1) { currentStep++; showTurn(); return; } @@ -783,19 +791,35 @@ function startTimer() { } function updateTimerUI(remaining) { + // TIME_PER_TURN may be 0 when the timer is "infinite"; bail out early + // to avoid dividing by zero or producing NaN widths. startTimer already + // handles this case, but other callers (tests, manual tweaks) might + // invoke the helper directly so we guard defensively here. + if (!TIME_PER_TURN) return; + var numEl = document.getElementById('timer-num'); var fillEl = document.getElementById('progress-fill'); + var pct = (remaining !== undefined) ? (remaining / TIME_PER_TURN * 100) : 100; var displayNum = (remaining !== undefined) ? Math.ceil(remaining) : TIME_PER_TURN; numEl.textContent = displayNum; fillEl.style.width = pct + '%'; - var urgent = displayNum <= (isHardcoreActive() ? 4 : 8); + + // urgency threshold is now relative to the total turn length instead of a + // hard‑coded 4/8 seconds. this makes custom timers (30s, 60s, …) feel + // sensible: the bar turns amber when ~25% time remains, regardless of mode. + var ratio = (remaining !== undefined) ? (remaining / TIME_PER_TURN) : 1; + var urgent = ratio < 0.25; numEl.classList.toggle('urgent', urgent); fillEl.classList.toggle('urgent', urgent); + + // In hardcore mode we also add a red "danger" state; keep it tied to the + // same relative threshold so that longer custom hardcore games still go + // red when only a small fraction of turns remains. if (isHardcoreActive()) { - numEl.classList.toggle('danger', displayNum <= 4); + numEl.classList.toggle('danger', ratio < 0.25); } else { numEl.classList.remove('danger'); } @@ -810,6 +834,7 @@ function autoPick() { disableAllCatButtons(); var PENALTY = 200; totalScore += PENALTY; + document.getElementById('total-score').textContent = totalScore; // keep UI in sync immediately var country = gameCountries[currentStep]; var cName = getCountryName(country); diff --git a/js/seed.js b/js/seed.js index 3aabd2b..960b2f1 100644 --- a/js/seed.js +++ b/js/seed.js @@ -90,12 +90,18 @@ function generateSeed() { function applyGameFromSeed(str) { var decoded = decodeSeed(str); if (!decoded) { alert(t('invalidSeed')); return false; } + + // extract indices early so we can validate before touching any global state var ci = decoded.countryIndices, ki = decoded.catIndices; if (ci.some(function(i){return i<0||i>=countriesDB.length;})) { alert(t('invalidSeedIdx')); return false; } if (ki.some(function(i){return i<0||i>=ALL_CATEGORIES.length;})) { alert(t('invalidSeedIdx')); return false; } + + // At this point the seed is syntactically correct *and* the indices refer to + // existing entries; safe to mutate globals. gameCountries = ci.map(function(i){return countriesDB[i];}); gameCategories = ki.map(function(i){return ALL_CATEGORIES[i];}); currentSeed = str; + if (!decoded.legacy) { var m = decoded.mode; document.body.classList.remove('hardcore','reverse-mode'); diff --git a/js/utils.js b/js/utils.js index 8d9f33a..d89b7b6 100644 --- a/js/utils.js +++ b/js/utils.js @@ -70,6 +70,11 @@ function encodeSeed(countryIdxs, catIdxs, mode, timeSecs) { } function decodeSeed(str) { + // Remember: this helper only converts the opaque base62 string back into + // numeric fields. it does **not** know anything about the current + // countriesDB/ALL_CATEGORIES arrays, so it cannot verify that the indices it + // returns are in-bounds. callers (e.g. applyGameFromSeed) must perform a + // separate sanity check before using the results. if (!str || typeof str !== 'string') return null; str = str.trim();