Minor bugs resolution - improved compatibility with mobile.

This commit is contained in:
Mathieu VIART 2026-03-02 14:22:55 +00:00
parent 1ffeeab8c8
commit 2576ffe4f7
7 changed files with 68 additions and 23 deletions

View file

@ -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 serverside score validation is performed either; the leaderboard
// is for fun only. if this service ever becomes competitive it should be
// hardened (ratelimit, score sanity checks, CAPTCHAs, etc.).
if (path === '/api/played' && req.method === 'GET') {
const date = todayUTC();
const ipHash = hashIP(getIP(req));

View file

@ -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
},
{

View file

@ -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 ──────────────────────────────────────────────────────────────

View file

@ -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');

View file

@ -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
// hardcoded 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);

View file

@ -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');

View file

@ -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();