// Game data const players = [ 'MESSI', 'RONALDO', 'SALAH', 'MBAPPE', 'NEYMAR', 'HAALAND', 'MODRIC', 'KANE', 'BENZEMA', 'PELE', 'XAVI', 'INIESTA', 'SUAREZ', 'ZIDANE', 'MBOMA', 'BRAZIL', 'FRANCE', 'SPAIN', 'EGYPT', 'ARGEN', 'HENRY', 'DAVID', 'ROBIN', 'LUKAK', 'POGBA', 'VIDAL', 'RAMOS', 'REUS', 'SONHE', 'ALABA', 'DIJON', 'LAMAR', 'MAHRE', 'SANEH', 'TORRE', 'GREAL', 'PEDRO', 'LEMAR', 'GOMES', 'LUCAS', 'MANÉ', 'KEITA', 'RIYAD', 'HAKIM', 'SAISS', 'SAKHO', 'FARIS', 'AMINE', 'ACHRA', 'SALEM', 'OMARU', 'TADIC', 'LUKAS', 'LORIS', 'LEMOS', 'NABIL', 'SAULI', 'JAMES', 'JESUS', 'OSCAR', 'ADRIA', 'MARIO', 'TONNY', 'NELSO', 'RENAN', 'LUCIO', 'LEOLO', 'PAULO', 'MARCE', 'NELLY', 'ROGER', 'SERGI', 'TONYU', 'YACIN', 'YASSI', 'YOUNS', 'SAUDI', 'KANTE', 'LAMAR', 'ADRIL', 'BAKAY', 'BARRY', 'CELSO', 'DANTE', 'ENNER', 'FABIO', 'GABRI', 'HUGOZ', 'ISCOO', 'JORGE', 'KENNY', 'LEMAR', 'MARCO', 'NELIO', 'OSCAR', 'PAULO', 'AMRAB', 'BENSE', 'BOUFA', 'CHOUI', 'Djalo', 'ELNEN', 'FADIL', 'GHANI', 'HAMDI', 'JABER', 'KABIR', 'LAMIN', 'MABRO', 'NAJIB', 'OUNAS', 'QADIR', 'RAFIK', 'SAHER', 'TALEB', 'YAHYA', 'ANASS', 'BAKIR', 'CHADI', 'DIOUF', 'ELIES', 'FARID', 'GHANI', 'HAMZA', 'ISLAM', 'JAWAD', 'KAMEL', 'LOTFI', 'MOUAD', 'NADER', 'OTMAN', 'QASIM', 'RABIE', 'SAHER', 'TAREK', 'YASSI', 'AYOUB', 'BADIS', 'CHAWI', 'DIOGO', 'ELHAD', 'FOUAD', 'HAFID', 'IMANE', 'JIHAD', 'KAMAL', 'LAMIN', 'MOUAD', 'NAJIB', 'OUNAS', 'QADIR', 'RABIE', 'SAHER', 'TAHAR', 'YAMIN', 'ZAKAR', 'AMMAR', 'BAKRI', 'CHAKI', 'DILAN', 'ELAMI', 'FADIL', 'GHADA', 'HAMID', 'IHABY', 'JADIR', 'KAMIR', 'LATIF', 'MOUHC', 'NADIR', 'OTMAN', 'QAMAR', 'RAFIK', 'SAMIR', 'TAHIR', 'YAMAL', // Football-related terms and clubs 'CAFAR','FIFAA','UEFAS','DERBY','FINAL','MATCH','GOALS','CLUBS','STADE','COACH','ARBIT','LIGUE','ALGER','MILAN','PARIS','LYONS','ROMAS','SANTO','AJAXS','DORIS','REALS','BARCA','INTER','CHELS','ARSEN','MANCH','JUVEA','RENNE','NAPOL','LEEDS','CELTI','RIVER','BOCAJ','SUDAN','TUNIS','MAROC','EGYPE','QATAR','SAUDI','OMANI','KUWAI','IRAQI','LIBYA','SUEZL','CAFES','ULTRA','FANSY','DRIBL','PASSE','SHOOT','CORNE','PENAL','TACTI','PRESS','DEFEN','ATTAQ','MIDFI','GOALK','VARRE','LINES','BENCH','SKILL','SPEED','POWER','CROSS','HEADS','VOLLE','WINGS','BLOCK','TUNEL','ZONAL','MARKS','PRESS','SCORE','WINER','LOSER','DRAWN','POINT','TABLE','ROUND','GROUP','STAGE','FINAL','FIRST','THIRD','FIFTH','SUPER','WORLD','AFRIC','ASIAN','EUROC','CUPSA','CWCUP','OLYMP','YOUTH','WOMEN','MENSA','LEGND','STARS','TEAMS','SQUAD','COACH','STAFF','AGENT','MEDIA','PRESS','FANSY', // Famous coaches (5-letter adapted) 'ZIDAN','GUARD','KLOPP','CONTE','SIMEO','TUCHE','LOPET','XAVIE','ARTET','GALTU','BENIT','LUCAS','HENRY','VITOR','JORGE','RENAR','PELLE','MOYES','LAMAR','FARID','SAHER','TUNIS','LEMAR','NAGEL','ROGER','LUISI','PABLO','OSCAR','PAULO','PEDRO','MARIO','ADRIA','TONNY','NELSO','RENAN','CELSO','DANTE','FABIO','GABRI','HUGOZ','JAMES','JESUS','NABIL','SAULI','RAMOS','VIDAL','POGBA','LUKAK','DAVID','HENRY','LAMIN','AMINE','KAMEL','LOTFI','MOUAD','NADER','OTMAN','QASIM','RABIE','TAREK','YASSI', // Arabic football names (5 letters) 'ميسي','صلاح','زيدان','محرز','بوفون','حكيمي','رياض','سفيان','بغداد','يوسف','أشرف','وليد','ياسين','أنس','فهد','سالم','ناصر','سعود','عيسى','فارس', // More Arabic names 'حكيم','أشرف','وليد','ياسر','فوزي','سليم','ربيع','نادر','عادل','سامي', 'ربيع','فوزي','عادل','سامي','ناصر', 'وفاق','أهلي','نصر','هلال','ترجي','زمالك','اتحاد','شباب','وحدة','رائد', 'هدفك','دفاع','هجوم','وسطك','تمرر','تسدد','حارس','مدرب','جمهور','ملعب' ]; const WORD_LENGTH = 5; const MAX_ATTEMPTS = 6; /** Résolu par rapport à la page (fonctionne en racine ou sous-dossier ex. /s/). */ const GAME_API_BASE = (() => { try { const a = document.createElement('a'); a.href = 'php-admin/api/game'; return String(a.href).replace(/\/+$/, ''); } catch (_) { return '/php-admin/api/game'; } })(); const GAME_MODE_STORAGE_KEY = 'footballGameMode_v1'; const ARCHIVE_MODE_KEY = 'footballArchiveMode'; const ARCHIVE_DATE_KEY = 'footballArchiveDate'; /** Fenêtre « derniers jours » + archive URL (alignée avec 30-days.html). */ const ARCHIVE_MAX_DAYS = 30; /** Modes affichés sur l’accueil (hard_6/7 = mots 6–7 lettres : prochaine étape). */ const GAME_MODES_UI = ['players', 'clubs', 'coaches']; const LATIN_FIVE = players.filter( (w) => typeof w === 'string' && w.length === WORD_LENGTH && /^[A-Z]{5}$/.test(w) ); const CLUB_TOKENS = [ 'CLUB', 'TEAM', 'LIGA', 'CUP', 'FIFA', 'UEFA', 'STAD', 'GOAL', 'FINA', 'DERBY', 'BENCH', 'MEDIA', 'VARRE', 'SQUAD', 'ULTRA', 'FANS', 'ARBIT', 'LIGUE', 'WORLD', 'ASIAN', 'AFRIC', 'EURO', 'OLYMP', 'YOUTH', 'WOMEN', 'MENSA', 'LEGND', 'STARS', 'GROUP', 'ROUND', 'TABLE', 'STAGE', 'SCORE', 'NAPOL', 'MILAN', 'PARIS', 'INTER', 'CHELS', 'ARSEN', 'JUVEA', 'AJAXS', 'BARCA', 'REALS', 'LEEDS', 'CELTI', 'RIVER', 'BOCAJ', 'LYONS', 'ROMAS', 'RENNE', 'DORIS', 'SANTO', 'ALGER', 'SPAIN', 'EGYPT', 'ARGEN', 'BRAZIL', 'FRANCE', 'SAUDI', 'MAROC', 'TUNIS', 'SUDAN', 'QATAR', 'KUWAI', 'IRAQI', 'LIBYA', 'OMANI', 'MATCH', 'POINT', 'CUPSA', 'CWCUP', 'MANCH', ]; const COACH_TOKENS = [ 'COACH', 'GUARD', 'KLOPP', 'CONTE', 'TUCHE', 'SIMEO', 'LOPET', 'XAVIE', 'ARTET', 'ZIDAN', 'GALTU', 'BENIT', 'VITOR', 'RENAR', 'MOYES', 'NAGEL', 'PELLE', 'STAFF', ]; function latinWordMatchesTokens(w, tokens) { return tokens.some((t) => w.includes(t)); } const wordsClubsLatin = LATIN_FIVE.filter((w) => latinWordMatchesTokens(w, CLUB_TOKENS)); const wordsCoachesLatin = LATIN_FIVE.filter((w) => latinWordMatchesTokens(w, COACH_TOKENS)); function getGameMode() { try { const s = localStorage.getItem(GAME_MODE_STORAGE_KEY); if (s === 'players' || s === 'clubs' || s === 'coaches' || s === 'hard_6' || s === 'hard_7') { return s; } } catch (_) { /* ignore */ } return 'players'; } function setGameMode(mode, opts) { const o = opts || {}; if (mode !== 'players' && mode !== 'clubs' && mode !== 'coaches' && mode !== 'hard_6' && mode !== 'hard_7') { mode = 'players'; } try { localStorage.setItem(GAME_MODE_STORAGE_KEY, mode); } catch (_) { /* ignore */ } syncGameModeButtons(); if (!o.skipPreferencePost && gameUserProfile && gameUserProfile.authenticated && gameUserProfile.csrf) { void postPreferredMode(mode); } } async function postPreferredMode(mode) { try { const r = await fetch(`${GAME_API_BASE}/preferences.php`, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ preferred_mode: mode, _csrf: gameUserProfile.csrf }), }); const d = await r.json().catch(() => ({})); if (d.csrf) { gameUserProfile.csrf = d.csrf; } if (d.ok && d.user) { gameUserProfile.user = d.user; } } catch (_) { /* ignore */ } } function syncGameModeButtons() { const m = getGameMode(); GAME_MODES_UI.forEach((id) => { const btn = document.getElementById(`game-mode-${id}`); if (btn) { btn.classList.toggle('active', id === m); } }); } function getTargetWordPool(mode) { if (mode === 'clubs') { return wordsClubsLatin.length >= 12 ? wordsClubsLatin : LATIN_FIVE; } if (mode === 'coaches') { return wordsCoachesLatin.length >= 8 ? wordsCoachesLatin : LATIN_FIVE; } if (mode === 'hard_6' || mode === 'hard_7') { return players; } return players; } function pickWordDailyFromPool(pool, dateKey, mode) { const five = pool.filter((w) => w.length === WORD_LENGTH); if (five.length === 0) { return 'HELLO'; } const seedKey = `${dateKey}|${mode}`; const rand = mulberry32(dateStringToSeed(seedKey)); const idx = Math.floor(rand() * five.length); return five[idx]; } async function resolveTargetWord(mode, dateKey) { const fallbackAnswer = pickWordDailyFromPool(getTargetWordPool(mode), dateKey, mode); const empty = { answer: fallbackAnswer, card: null, puzzleDate: dateKey }; try { const u = new URL(`${GAME_API_BASE}/daily-word.php`, window.location.origin); u.searchParams.set('mode', mode); u.searchParams.set('date', dateKey); const r = await fetch(u.toString(), { credentials: 'same-origin', cache: 'no-store' }); const d = await r.json().catch(() => ({})); if (r.ok && d.ok && d.answer && String(d.answer).length === WORD_LENGTH) { const card = d.card && typeof d.card === 'object' && !Array.isArray(d.card) ? d.card : null; return { answer: String(d.answer), card, puzzleDate: d.puzzle_date ? String(d.puzzle_date) : dateKey, }; } } catch (_) { /* fallback client */ } return empty; } // Language translations const translations = { arabic: { mainTitle: 'Wordle كرة القدم - لعبة تخمين اللاعبين والأندية والمنتخبات', mainDesc: 'استمتع مع لعبة Wordle كرة القدم: تحدى أصدقاءك في تخمين أسماء اللاعبين، الأندية، المنتخبات والمصطلحات الكروية الشهيرة. لعبة ذكاء وتسلية لعشاق كرة القدم باللغة العربية والفرنسية والإنجليزية!', attemptsLeft: 'المحاولات المتبقية: ', restart: 'إعادة اللعب', win: 'مبروك! لقد فزت!', lose: 'انتهت المحاولات! الإجابة كانت: ', invalid: 'اسم غير صالح', notEnough: 'أدخل 5 أحرف', articlesHeading: 'مقالات', articlesLoading: 'جاري التحميل…', articlesEmpty: 'لا توجد مقالات بعد.', articlesAdminHint: 'في الإدارة: أنشئ مقالاً واختر حالة «منشور».', articlesError: 'تعذر تحميل المقالات.', articlesBadJson: 'الخادم لم يرجع JSON. تحقق من php-admin وملف config.php.', articlesSeeAll: 'جميع المقالات', articlesReadMore: 'اقرأ', dailyChallenge: 'تحدي اليوم', shareResult: 'مشاركة النتيجة', shareCopied: 'تم نسخ النتيجة!', shareFailed: 'تعذر النسخ — انسخ يدوياً.', statsGames: 'ألعاب', statsWins: 'فوز', statsStreak: 'سلسلة', statsBest: 'أفضل سلسلة', gameModeLabel: 'وضع اللعب', gameModePlayers: 'لاعبون', gameModeClubs: 'أندية', gameModeCoaches: 'مدربون', archiveReplay: 'أرشيف', puzzleCardMore: 'المزيد', menuHome: 'الرئيسية', menu30Days: 'آخر 30 يومًا', menuArchives: 'الأرشيف', menuLeaderboard: 'لوحة الصدارة', menuAccount: 'حساب اللاعب', menuArticles: 'المقالات', menuContact: 'اتصل بنا', menuPrivacy: 'سياسة الخصوصية', menuAbout: 'من نحن', menuHowToPlay: 'كيفية اللعب', }, english: { mainTitle: 'Football Wordle - Guess the Players, Clubs, and Teams!', mainDesc: 'Enjoy Football Wordle: Challenge your friends to guess the names of players, clubs, teams, and famous football terms. A fun and smart game for football fans in English, Arabic, and French!', attemptsLeft: 'Attempts left: ', restart: 'Play Again', win: 'Congratulations! You won!', lose: 'Game Over! The answer was: ', invalid: 'Invalid name', notEnough: 'Enter 5 letters', articlesHeading: 'Articles', articlesLoading: 'Loading…', articlesEmpty: 'No articles yet.', articlesAdminHint: 'In admin, create an article and set status to Published.', articlesError: 'Could not load articles.', articlesBadJson: 'Server did not return JSON. Check php-admin and config.php.', articlesSeeAll: 'All articles', articlesReadMore: 'Read', dailyChallenge: 'Daily challenge', shareResult: 'Share result', shareCopied: 'Copied to clipboard!', shareFailed: 'Could not copy — select manually.', statsGames: 'Played', statsWins: 'Wins', statsStreak: 'Streak', statsBest: 'Best streak', gameModeLabel: 'Mode', gameModePlayers: 'Players', gameModeClubs: 'Clubs', gameModeCoaches: 'Coaches', archiveReplay: 'Archive', puzzleCardMore: 'Learn more', menuHome: 'Home', menu30Days: 'Last 30 days', menuArchives: 'Archives', menuLeaderboard: 'Leaderboard', menuAccount: 'Account', menuArticles: 'Articles', menuContact: 'Contact Us', menuPrivacy: 'Privacy Policy', menuAbout: 'About Us', menuHowToPlay: 'How to Play', }, french: { mainTitle: 'Wordle Football - Devine les joueurs, clubs et équipes!', mainDesc: 'Profitez de Wordle Football : défiez vos amis de deviner les noms des joueurs, clubs, équipes et termes célèbres du football. Un jeu amusant et intelligent pour les fans de football en français, anglais et arabe!', attemptsLeft: 'Tentatives restantes: ', restart: 'Rejouer', win: 'Félicitations! Vous avez gagné!', lose: 'Partie terminée! La réponse était: ', invalid: 'Nom invalide', notEnough: 'Entrez 5 lettres', articlesHeading: 'Articles', articlesLoading: 'Chargement…', articlesEmpty: 'Aucun article pour le moment.', articlesAdminHint: 'Dans l’admin : créez un article avec le statut Publié.', articlesError: 'Impossible de charger les articles.', articlesBadJson: 'Réponse invalide (pas du JSON). Vérifiez php-admin et config.php.', articlesSeeAll: 'Tous les articles', articlesReadMore: 'Lire', dailyChallenge: 'Défi du jour', shareResult: 'Partager le résultat', shareCopied: 'Copié dans le presse-papiers !', shareFailed: 'Impossible de copier.', statsGames: 'Parties', statsWins: 'Victoires', statsStreak: 'Série', statsBest: 'Meilleure série', gameModeLabel: 'Mode', gameModePlayers: 'Joueurs', gameModeClubs: 'Clubs', gameModeCoaches: 'Entraîneurs', archiveReplay: 'Archive', puzzleCardMore: 'En savoir plus', menuHome: 'Accueil', menu30Days: '30 derniers jours', menuArchives: 'Archives', menuLeaderboard: 'Classement', menuAccount: 'Compte', menuArticles: 'Articles', menuContact: 'Contact', menuPrivacy: 'Confidentialité', menuAbout: 'À propos', menuHowToPlay: 'Comment jouer', } }; const LANG_STORAGE_KEY = 'footballSiteLang'; const STATS_KEY = 'footballWordleStats_v1'; const THEME_KEY = 'footballTheme'; /** @type {{ authenticated: boolean, stats: object|null, user: object|null, csrf: string }|null} */ let gameUserProfile = null; let gameStartedAtMs = 0; /** Nombre d’articles affiches sur l’accueil (apercu uniquement). */ const ARTICLES_HOME_PREVIEW_COUNT = 2; let shareLines = []; let lastGameOutcomeWin = false; function getDefaultLanguageFromPage() { if (typeof window === 'undefined' || !window.location) { return 'french'; } const path = String(window.location.pathname || '').toLowerCase(); if (path.endsWith('index-en.html')) { return 'english'; } if (path.endsWith('index-fr.html')) { return 'french'; } const htmlLang = (typeof document !== 'undefined' && document.documentElement && document.documentElement.lang) ? String(document.documentElement.lang).toLowerCase() : ''; if (htmlLang.startsWith('fr')) { return 'french'; } if (htmlLang.startsWith('ar')) { return 'arabic'; } return 'french'; } function getStoredLanguage() { try { const s = localStorage.getItem(LANG_STORAGE_KEY); if (s === 'arabic' || s === 'english' || s === 'french') { return s; } } catch (_) { /* ignore */ } return getDefaultLanguageFromPage(); } /** Aligne lang/dir du document avec la langue du jeu (évite ar+RTL avec UI anglais). */ function syncDocumentLangDir() { if (typeof document === 'undefined' || !document.documentElement) { return; } if (currentLanguage === 'arabic') { document.documentElement.lang = 'ar'; document.documentElement.setAttribute('dir', 'rtl'); } else if (currentLanguage === 'french') { document.documentElement.lang = 'fr'; document.documentElement.setAttribute('dir', 'ltr'); } else { document.documentElement.lang = 'en'; document.documentElement.setAttribute('dir', 'ltr'); } } let currentLanguage = getStoredLanguage(); let targetWord = ''; /** Données affichées après la partie si `daily-word` renvoie `card` (admin). */ let puzzleCardMeta = null; let attempts = MAX_ATTEMPTS; let currentAttempt = 0; let gameOver = false; let guesses = Array(MAX_ATTEMPTS).fill('').map(() => ''); const gameBoard = document.querySelector('.game-board'); const messageElement = document.getElementById('message'); const attemptsElement = document.getElementById('attempts-left'); const restartButton = document.getElementById('restart'); const languageButtons = document.querySelectorAll('.language-selector button'); const keyboardContainer = document.querySelector('.keyboard'); const KEYBOARD_LAYOUT = [ ['Q','W','E','R','T','Y','U','I','O','P'], ['A','S','D','F','G','H','J','K','L'], ['ENTER','Z','X','C','V','B','N','M','BACK'] ]; const ARABIC_KEYBOARD_LAYOUT = [ ['ض','ص','ث','ق','ف','غ','ع','ه','خ','ح'], ['ج','د','ش','س','ي','ب','ل','ا','ت'], ['ENTER','ن','م','ك','ط','ئ','ء','ؤ','BACK'] ]; function getLocalDateKey() { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } function formatYmdLocal(d) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } function getMinArchiveDateStr() { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - ARCHIVE_MAX_DAYS); return formatYmdLocal(d); } /** Date du légume affichée / résolue (archive ou aujourd’hui). */ function getEffectivePuzzleDate() { if (isArchiveMode()) { try { const a = localStorage.getItem(ARCHIVE_DATE_KEY); if (a && /^\d{4}-\d{2}-\d{2}$/.test(a)) { return a; } } catch (_) { /* ignore */ } } return getLocalDateKey(); } /** * Applique `?date=` et `?mode=` ; conserve un archive localStorage valide si pas de `date`. */ function applyUrlGameOverrides() { const p = new URLSearchParams(window.location.search); const dateParam = p.get('date'); const modeParam = p.get('mode'); const today = getLocalDateKey(); const minD = getMinArchiveDateStr(); const applyModeFromUrl = () => { if (modeParam && GAME_MODES_UI.includes(modeParam) && modeParam !== getGameMode()) { setGameMode(modeParam, { skipPreferencePost: true }); } }; if (dateParam && /^\d{4}-\d{2}-\d{2}$/.test(dateParam)) { if (dateParam > today || dateParam < minD) { clearArchiveMode(); applyModeFromUrl(); return; } if (dateParam === today) { clearArchiveMode(); applyModeFromUrl(); return; } const m = modeParam && GAME_MODES_UI.includes(modeParam) ? modeParam : getGameMode(); try { localStorage.setItem(ARCHIVE_DATE_KEY, dateParam); localStorage.setItem(ARCHIVE_MODE_KEY, m); } catch (_) { /* ignore */ } if (m !== getGameMode()) { setGameMode(m, { skipPreferencePost: true }); } return; } try { const ad = localStorage.getItem(ARCHIVE_DATE_KEY); if (!ad || !/^\d{4}-\d{2}-\d{2}$/.test(ad)) { clearArchiveMode(); } else if (ad > today || ad < minD || ad === today) { clearArchiveMode(); } } catch (_) { clearArchiveMode(); } applyModeFromUrl(); } function getDailyPuzzleNumber() { return getPuzzleNumberForYmd(getEffectivePuzzleDate()); } function getPuzzleNumberForYmd(ymd) { const parts = String(ymd).split('-').map((x) => parseInt(x, 10)); if (parts.length !== 3 || parts.some((n) => Number.isNaN(n))) { return 1; } const d = new Date(parts[0], parts[1] - 1, parts[2]); d.setHours(0, 0, 0, 0); const origin = new Date(2024, 0, 1); origin.setHours(0, 0, 0, 0); return Math.max(1, Math.round((d.getTime() - origin.getTime()) / 86400000) + 1); } function dateStringToSeed(str) { let h = 2166136261 >>> 0; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; } return h >>> 0; } function mulberry32(seed) { return function rand() { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function rowElementToEmoji(rowEl) { let s = ''; for (let i = 0; i < WORD_LENGTH; i++) { const t = rowEl.children[i]; if (t.classList.contains('correct')) s += '🟩'; else if (t.classList.contains('present')) s += '🟨'; else s += '⬛'; } return s; } function loadStats() { try { const raw = localStorage.getItem(STATS_KEY); if (!raw) return { games: 0, wins: 0, streak: 0, maxStreak: 0 }; const o = JSON.parse(raw); return { games: Number(o.games) || 0, wins: Number(o.wins) || 0, streak: Number(o.streak) || 0, maxStreak: Number(o.maxStreak) || 0, }; } catch (_) { return { games: 0, wins: 0, streak: 0, maxStreak: 0 }; } } function saveStatsAfterGame(won) { if (isArchiveMode()) { return; } const dayKey = `footballDailyStats_${getEffectivePuzzleDate()}_${getGameMode()}`; try { if (localStorage.getItem(dayKey)) { return; } } catch (_) { /* continue without dedupe */ } const s = loadStats(); s.games += 1; if (won) { s.wins += 1; s.streak += 1; s.maxStreak = Math.max(s.maxStreak, s.streak); } else { s.streak = 0; } try { localStorage.setItem(STATS_KEY, JSON.stringify(s)); localStorage.setItem(dayKey, '1'); } catch (_) { /* ignore */ } } async function refreshGameUserProfile() { try { const r = await fetch(`${GAME_API_BASE}/me.php`, { credentials: 'same-origin', cache: 'no-store' }); const d = await r.json().catch(() => ({})); if (d.ok && d.csrf) { gameUserProfile = { authenticated: !!d.authenticated, stats: d.stats || null, user: d.user || null, csrf: d.csrf, }; if (d.authenticated && d.user && d.user.preferred_mode) { try { if (localStorage.getItem(GAME_MODE_STORAGE_KEY) === null) { setGameMode(d.user.preferred_mode, { skipPreferencePost: true }); } } catch (_) { /* ignore */ } } } else { gameUserProfile = { authenticated: false, stats: null, user: null, csrf: d.csrf || '' }; } } catch (_) { gameUserProfile = null; } syncGameModeButtons(); updateStatsLine(); } async function submitRankedGameFinish(won) { if (!gameUserProfile || !gameUserProfile.authenticated) { return; } const attempts = won ? currentAttempt + 1 : MAX_ATTEMPTS; const duration = gameStartedAtMs ? Math.max(0, Math.round((Date.now() - gameStartedAtMs) / 1000)) : null; const mode = getGameMode(); const isArchive = isArchiveMode(); const body = { outcome: won ? 'win' : 'loss', attempts_used: attempts, duration_seconds: duration, game_mode: mode, counts_for_streak: isArchive ? 0 : 1, counts_for_leaderboard: isArchive ? 0 : 1, _csrf: gameUserProfile.csrf, }; if (isArchive) { body.puzzle_date = getEffectivePuzzleDate(); } try { const r = await fetch(`${GAME_API_BASE}/finish.php`, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const d = await r.json().catch(() => ({})); if (d.csrf) { gameUserProfile.csrf = d.csrf; } if (d.ok && d.stats) { gameUserProfile.stats = d.stats; } else if (r.status === 409 && d.error === 'already_finished_today') { await refreshGameUserProfile(); } } catch (_) { /* réseau : stats locales inchangées */ } if (isArchive) { clearArchiveMode(); } updateStatsLine(); } function updateStatsLine() { const el = document.getElementById('stats-line'); if (!el) return; const t = translations[currentLanguage]; if (gameUserProfile && gameUserProfile.authenticated && gameUserProfile.stats) { const s = gameUserProfile.stats; el.textContent = `${t.statsGames}: ${s.total_games} · ${t.statsWins}: ${s.total_wins} · ${t.statsStreak}: ${s.current_streak_days} · ${t.statsBest}: ${s.best_streak_days}`; return; } const s = loadStats(); el.textContent = `${t.statsGames}: ${s.games} · ${t.statsWins}: ${s.wins} · ${t.statsStreak}: ${s.streak} · ${t.statsBest}: ${s.maxStreak}`; } function updateDailyLabel() { const el = document.getElementById('daily-puzzle-label'); if (!el) return; const t = translations[currentLanguage]; const n = getDailyPuzzleNumber(); const d = getEffectivePuzzleDate(); const mode = getGameMode(); let modeLabel = t.gameModePlayers; if (mode === 'clubs') { modeLabel = t.gameModeClubs; } else if (mode === 'coaches') { modeLabel = t.gameModeCoaches; } else if (mode === 'hard_6' || mode === 'hard_7') { modeLabel = mode === 'hard_6' ? 'Hard 6' : 'Hard 7'; } const prefix = isArchiveMode() && t.archiveReplay ? `${t.archiveReplay} · ` : ''; el.textContent = `${prefix}${t.dailyChallenge} · #${n} · ${d} · ${t.gameModeLabel}: ${modeLabel}`; } function isArchiveMode() { try { const archiveDate = localStorage.getItem(ARCHIVE_DATE_KEY); const archiveMode = localStorage.getItem(ARCHIVE_MODE_KEY); return !!(archiveDate && archiveMode); } catch (_) { return false; } } function clearArchiveMode() { try { localStorage.removeItem(ARCHIVE_DATE_KEY); localStorage.removeItem(ARCHIVE_MODE_KEY); } catch (_) { /* ignore */ } } async function initGame() { if (!gameBoard || !keyboardContainer || !messageElement || !attemptsElement || !restartButton) { return; } puzzleCardMeta = null; hidePuzzleCardPanel(); const mode = getGameMode(); const dateKey = getEffectivePuzzleDate(); try { const resolved = await resolveTargetWord(mode, dateKey); targetWord = resolved.answer; puzzleCardMeta = { card: resolved.card, puzzle_date: resolved.puzzleDate || dateKey }; } catch (_) { targetWord = pickWordDailyFromPool(getTargetWordPool(mode), dateKey, mode); puzzleCardMeta = { card: null, puzzle_date: dateKey }; } attempts = MAX_ATTEMPTS; currentAttempt = 0; gameOver = false; gameStartedAtMs = Date.now(); shareLines = []; guesses = Array(MAX_ATTEMPTS).fill('').map(() => ''); gameBoard.innerHTML = ''; messageElement.textContent = ''; restartButton.classList.add('hidden'); const shareBtn = document.getElementById('share-result'); if (shareBtn) shareBtn.classList.add('hidden'); renderBoard(); renderKeyboard(); updateUI(); } function renderBoard() { if (!gameBoard) return; gameBoard.innerHTML = ''; for (let i = 0; i < MAX_ATTEMPTS; i++) { const row = document.createElement('div'); row.className = 'row'; for (let j = 0; j < WORD_LENGTH; j++) { const tile = document.createElement('div'); tile.className = 'tile'; if (guesses[i][j]) tile.textContent = guesses[i][j]; row.appendChild(tile); } gameBoard.appendChild(row); } } function renderKeyboard() { if (!keyboardContainer) return; keyboardContainer.innerHTML = ''; let layout = KEYBOARD_LAYOUT; if (currentLanguage === 'arabic') layout = ARABIC_KEYBOARD_LAYOUT; layout.forEach(row => { const rowDiv = document.createElement('div'); rowDiv.className = 'keyboard-row'; row.forEach(key => { const keyBtn = document.createElement('button'); keyBtn.className = 'key'; if (key === 'ENTER' || key === 'BACK') keyBtn.classList.add('wide'); keyBtn.textContent = key === 'BACK' ? '⌫' : (key === 'ENTER' ? (currentLanguage === 'arabic' ? 'إدخال' : 'ENTER') : key); keyBtn.dataset.key = key; keyBtn.addEventListener('click', () => handleKey(key)); rowDiv.appendChild(keyBtn); }); keyboardContainer.appendChild(rowDiv); }); // Set RTL for Arabic if (currentLanguage === 'arabic') { keyboardContainer.dir = 'rtl'; if (gameBoard) gameBoard.dir = 'rtl'; } else { keyboardContainer.dir = 'ltr'; if (gameBoard) gameBoard.dir = 'ltr'; } } function updateUI() { syncDocumentLangDir(); const mainTitle = document.getElementById('main-title'); const mainDesc = document.getElementById('main-desc'); if (mainTitle) { mainTitle.textContent = translations[currentLanguage].mainTitle; mainTitle.dir = currentLanguage === 'arabic' ? 'rtl' : 'ltr'; } if (mainDesc) { mainDesc.textContent = translations[currentLanguage].mainDesc; mainDesc.dir = currentLanguage === 'arabic' ? 'rtl' : 'ltr'; } if (attemptsElement) { attemptsElement.dir = currentLanguage === 'arabic' ? 'rtl' : 'ltr'; attemptsElement.textContent = translations[currentLanguage].attemptsLeft + (MAX_ATTEMPTS - currentAttempt); } if (restartButton) { restartButton.textContent = translations[currentLanguage].restart; } const shareBtn = document.getElementById('share-result'); if (shareBtn) { shareBtn.textContent = translations[currentLanguage].shareResult; } const gmLabel = document.getElementById('game-mode-ui-label'); if (gmLabel) { gmLabel.textContent = translations[currentLanguage].gameModeLabel; } updateGameModeButtonsText(); updateMenuTranslations(); updateArticlesHeading(); syncGameModeButtons(); updateDailyLabel(); updateStatsLine(); } function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** Autorise seulement http(s) ou chemin absolu sur le site (carte admin). */ function sanitizeCardUrl(raw) { const s = String(raw || '').trim(); if (!s) { return ''; } if (s.startsWith('/')) { return s; } if (/^https:\/\//i.test(s)) { return s; } return ''; } function hidePuzzleCardPanel() { const el = document.getElementById('puzzle-card-panel'); if (!el) { return; } el.innerHTML = ''; el.classList.add('hidden'); el.setAttribute('aria-hidden', 'true'); } /** * Affiche la carte `card_json` après win/loss (champs optionnels : title, subtitle, body|description, image|image_url, url+link_label ou link:{url,label}). */ function renderPuzzleCardAfterGame() { const host = document.getElementById('puzzle-card-panel'); if (!host) { return; } host.innerHTML = ''; if (!puzzleCardMeta || !puzzleCardMeta.card || typeof puzzleCardMeta.card !== 'object') { host.classList.add('hidden'); host.setAttribute('aria-hidden', 'true'); return; } const c = puzzleCardMeta.card; const title = String(c.title != null ? c.title : '').trim(); const subtitle = String(c.subtitle != null ? c.subtitle : '').trim(); const bodyRaw = c.body != null ? c.body : c.description; const body = String(bodyRaw != null ? bodyRaw : '').trim(); const imgSrc = sanitizeCardUrl(c.image || c.image_url); let linkUrl = sanitizeCardUrl(c.url); if (!linkUrl && c.link && typeof c.link === 'object') { linkUrl = sanitizeCardUrl(c.link.url); } const linkLabelRaw = c.link_label || (c.link && typeof c.link === 'object' ? c.link.label : '') || linkUrl; const linkLabel = String(linkLabelRaw || '').trim(); if (!title && !subtitle && !body && !imgSrc && !linkUrl) { host.classList.add('hidden'); host.setAttribute('aria-hidden', 'true'); return; } const inner = document.createElement('div'); inner.className = 'puzzle-card-panel__inner'; if (imgSrc) { const wrap = document.createElement('div'); wrap.className = 'puzzle-card-panel__imgwrap'; const img = document.createElement('img'); img.className = 'puzzle-card-panel__img'; img.src = imgSrc; img.alt = title || subtitle || ''; img.loading = 'lazy'; wrap.appendChild(img); inner.appendChild(wrap); } if (title) { const h = document.createElement('h3'); h.className = 'puzzle-card-panel__title'; h.textContent = title; inner.appendChild(h); } if (subtitle) { const p = document.createElement('p'); p.className = 'puzzle-card-panel__subtitle'; p.textContent = subtitle; inner.appendChild(p); } if (body) { const p = document.createElement('p'); p.className = 'puzzle-card-panel__body'; p.textContent = body; inner.appendChild(p); } if (linkUrl) { const a = document.createElement('a'); a.className = 'puzzle-card-panel__link'; a.href = linkUrl; a.rel = 'noopener noreferrer'; if (/^https?:\/\//i.test(linkUrl)) { a.target = '_blank'; } a.textContent = linkLabel || translations[currentLanguage].puzzleCardMore; inner.appendChild(a); } host.appendChild(inner); host.classList.remove('hidden'); host.setAttribute('aria-hidden', 'false'); } /** Base absolue si `data-api-root` est défini sur #articles-panel (ex. /s/). Sinon null. */ function articlesManualBase(panel) { if (!panel) return null; const manual = (panel.dataset.apiRoot || '').trim(); if (!manual) return null; if (manual.startsWith('http://') || manual.startsWith('https://')) { return manual.endsWith('/') ? manual : `${manual}/`; } const path = manual.startsWith('/') ? manual : `/${manual}`; return new URL(path.endsWith('/') ? path : `${path}/`, window.location.origin).href; } /** URLs JSON à essayer : d’abord relative à la page (fiable), puis racine du domaine (si php-admin à la racine). */ function articlesJsonUrlCandidates(panel) { const rel = panel.dataset.json || 'php-admin/articles-json.php'; const page = window.location.href.split('#')[0]; const candidates = []; // 1. Chemin relatif à la page (ex: php-admin/articles-json.php) candidates.push(new URL(rel, page).href); // 2. Chemin absolu depuis la racine (ex: /php-admin/articles-json.php) const rootRel = rel.startsWith('/') ? rel : '/' + rel; candidates.push(new URL(rootRel, window.location.origin).href); // 3. Dossier manuel si défini const manual = articlesManualBase(panel); if (manual) { candidates.push(new URL(rel, manual).href); } return [...new Set(candidates)]; } /** Dossier contenant le JSON (pour résoudre article.php au même endroit). */ function articlesPhpAdminDirFromJsonUrl(jsonUrl) { return new URL('.', jsonUrl).href; } async function loadArticlesList() { const panel = document.getElementById('articles-panel'); const flexEl = document.getElementById('articles-preview-flex'); if (!panel || !flexEl) return; const t = translations[currentLanguage]; const jsonCandidates = articlesJsonUrlCandidates(panel); updateArticlesHeading(); flexEl.innerHTML = `
${escapeHtml(t.articlesAdminHint)}
${escapeHtml(t.articlesBadJson)}
` : ''; flexEl.innerHTML = `