geoquiz/js/utils.js
Joshua Hirsig ea561cd0bd Modularize index.html into separate CSS/JS files
Split the monolithic 5510-line index.html into 14 separate files:
- css/styles.css (1085 lines) - all styling
- js/translations.js - i18n (FR/EN/UA/DE)
- js/data.js - categories, presets, fallback data
- js/utils.js - shuffle, seed encoding/decoding
- js/seed.js - seed generation algorithms
- js/ribbon.js - country/category ribbon selectors
- js/setup.js - custom panel, mode selection, presets
- js/game.js - core game loop, state, timer
- js/game-end.js - end screen, Hungarian algorithm, odometer
- js/daily.js - daily mode, seeded RNG, leaderboard
- js/hints.js - tooltips, country descriptions
- js/ui-effects.js - animations, ripple, particles
- js/mobile.js - responsive ribbon tabs

Also allow .docx/.xlsx in .gitignore (used as dev resources).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:53:23 +01:00

113 lines
4.6 KiB
JavaScript

// ─── SEED ─────────────────────────────────────────────────────────────────────
function shuffle(arr) { return arr.slice().sort(function() { return Math.random() - 0.5; }); }
// ─── SEED ENGINE v2 ───────────────────────────────────────────────────────────
// Format interne (avant encodage) :
// [version:1][mode:1][time:2][n:1][countries:n*2][cats:n*2][checksum:2]
// mode : 0=normal 1=hardcore 2=reverse
// time : secondes (0=infini)
// Encodage : Base62 → chaîne opaque, non-lisible, non-modifiable à la main
var SEED_VERSION = 2;
var BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
function bytesToBase62(bytes) {
var digits = [0];
for (var b = 0; b < bytes.length; b++) {
var carry = bytes[b];
for (var d = 0; d < digits.length; d++) {
carry += digits[d] * 256;
digits[d] = carry % 62;
carry = Math.floor(carry / 62);
}
while (carry > 0) { digits.push(carry % 62); carry = Math.floor(carry / 62); }
}
var result = '';
for (var d = digits.length - 1; d >= 0; d--) result += BASE62[digits[d]];
return result || '0';
}
function base62ToBytes(str, expectedLen) {
var digits = [];
for (var i = 0; i < str.length; i++) {
var v = BASE62.indexOf(str[i]);
if (v < 0) return null;
digits.push(v);
}
var bytes = [0];
for (var d = 0; d < digits.length; d++) {
var carry = digits[d];
for (var b = 0; b < bytes.length; b++) {
carry += bytes[b] * 62;
bytes[b] = carry & 0xFF;
carry >>= 8;
}
while (carry > 0) { bytes.push(carry & 0xFF); carry >>= 8; }
}
bytes.reverse();
while (bytes.length < expectedLen) bytes.unshift(0);
if (bytes.length > expectedLen) bytes = bytes.slice(bytes.length - expectedLen);
return bytes;
}
function encodeSeed(countryIdxs, catIdxs, mode, timeSecs) {
var modeCode = (mode === 'hardcore') ? 1 : (mode === 'reverse') ? 2 : 0;
var time = (timeSecs === undefined || timeSecs === null) ? 20 : timeSecs;
var n = countryIdxs.length;
var bytes = [];
bytes.push(SEED_VERSION);
bytes.push(modeCode);
bytes.push(time & 0xFF);
bytes.push((time >> 8) & 0xFF);
bytes.push(n);
for (var i = 0; i < n; i++) { bytes.push(countryIdxs[i] & 0xFF); bytes.push((countryIdxs[i] >> 8) & 0xFF); }
for (var i = 0; i < n; i++) { bytes.push(catIdxs[i] & 0xFF); bytes.push((catIdxs[i] >> 8) & 0xFF); }
var checksum = 0;
for (var b = 0; b < bytes.length; b++) checksum = (checksum + bytes[b]) & 0xFFFF;
bytes.push(checksum & 0xFF);
bytes.push((checksum >> 8) & 0xFF);
return bytesToBase62(bytes);
}
function decodeSeed(str) {
if (!str || typeof str !== 'string') return null;
str = str.trim();
// Rétrocompatibilité ancien format "12-45_0-5"
if (/^\d[\d-]*_[\d-]*\d$/.test(str)) {
var parts = str.split('_');
if (parts.length !== 2) return null;
var ci = parts[0].split('-').map(Number);
var ki = parts[1].split('-').map(Number);
if (ci.length !== ki.length || ci.length < 2) return null;
if (ci.some(isNaN) || ki.some(isNaN)) return null;
return { countryIndices: ci, catIndices: ki, mode: 'normal', timeSecs: 20, legacy: true };
}
// Format v2 Base62 — taille header = 5 + n*4 + 2
// On essaie n=2..16 jusqu'à trouver un checksum valide
for (var n = 2; n <= 16; n++) {
var expectedLen = 5 + n * 4 + 2;
var bytes = base62ToBytes(str, expectedLen);
if (!bytes || bytes.length !== expectedLen) continue;
if (bytes[0] !== SEED_VERSION) continue;
if (bytes[4] !== n) continue;
var checksumStored = bytes[expectedLen - 2] + (bytes[expectedLen - 1] << 8);
var checksumCalc = 0;
for (var b = 0; b < expectedLen - 2; b++) checksumCalc = (checksumCalc + bytes[b]) & 0xFFFF;
if (checksumStored !== checksumCalc) continue;
var modeCode = bytes[1];
var timeSecs = bytes[2] + (bytes[3] << 8);
var ci = [], ki = [], offset = 5;
for (var i = 0; i < n; i++) { ci.push(bytes[offset] + (bytes[offset+1] << 8)); offset += 2; }
for (var i = 0; i < n; i++) { ki.push(bytes[offset] + (bytes[offset+1] << 8)); offset += 2; }
var mode = modeCode === 1 ? 'hardcore' : modeCode === 2 ? 'reverse' : 'normal';
return { countryIndices: ci, catIndices: ki, mode: mode, timeSecs: timeSecs, legacy: false };
}
return null;
}
function _currentGameMode() {
if (gameMode === 'custom') return customSubMode || 'normal';
return gameMode;
}