quadle

Loading today's puzzle…

quadle

Unable to load today's puzzle.
Please check your connection and refresh.

quadle

A daily equation guessing game.

Choose today’s puzzle

100% Ad FREE!

Please support if you enjoy!

How it works

  1. Enter a true equation using the calculator pad.
  2. Green means the symbol is correct and in the right spot.
  3. Gold means the symbol appears somewhere else.
  4. Grey means it is not in today's answer.
Mock example: three guesses, then solved.
Equadle
Next puzzle
Length
Guesses
Answer
Operators sit in the right column. Result hints appear as button borders.
const MODES = { easy: { label: "Easy", guesses: 6, minLength: 5, maxLength: 8, operatorCounts: [1], maxNumber: 99, targetMaxAbs: 999 }, medium: { label: "Hard", guesses: 7, minLength: 8, maxLength: 12, operatorCounts: [1, 2], maxNumber: 99, targetMaxAbs: 9999 }, hard: { label: "Extreme", guesses: 8, minLength: 12, maxLength: 18, operatorCounts: [2, 3], maxNumber: 999, targetMaxAbs: 99999 } }; const ALLOWED = "0123456789+-×÷="; const CALC_KEYS = [ { label: "7", type: "num" }, { label: "8", type: "num" }, { label: "9", type: "num" }, { label: "÷", type: "op" }, { label: "4", type: "num" }, { label: "5", type: "num" }, { label: "6", type: "num" }, { label: "×", type: "op" }, { label: "1", type: "num" }, { label: "2", type: "num" }, { label: "3", type: "num" }, { label: "-", type: "op" }, { label: "0", type: "num", span: 3 }, { label: "+", type: "op" }, { label: "⌫", type: "control", span: 3 }, { label: "=", type: "equals" } ]; const splashView = document.getElementById("splashView"); const gameView = document.getElementById("gameView"); const splashDate = document.getElementById("splashDate"); const newDayLabel = document.getElementById("newDayLabel"); const exampleBoard = document.getElementById("exampleBoard"); const themeToggle = document.getElementById("themeToggle"); const backBtn = document.getElementById("backBtn"); const resetBtn = document.getElementById("resetBtn"); const entryGuessBtn = document.getElementById("entryGuessBtn"); const board = document.getElementById("board"); const answerSection = document.getElementById("answerSection"); const answerRow = document.getElementById("answerRow"); const entryRow = document.getElementById("entryRow"); const message = document.getElementById("message"); const calculator = document.getElementById("calculator"); const dateLabel = document.getElementById("dateLabel"); const lengthLabel = document.getElementById("lengthLabel"); const guessLabel = document.getElementById("guessLabel"); const gameTitle = document.getElementById("gameTitle"); const gameScroll = document.getElementById("gameScroll"); let currentMode = "easy"; let puzzle = null; let state = null; let entry = ""; applyInitialTheme(); cleanupOldDailyState(); renderSplash(); function pad2(n) { return String(n).padStart(2, "0"); } function localDateKey(date = new Date()) { return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`; } function displayDate(date = new Date()) { return date.toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric" }); } function hashString(str) { let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; } function seededRandom(seed) { return function() { seed |= 0; seed = seed + 0x6D2B79F5 | 0; let t = Math.imul(seed ^ seed >>> 15, 1 | seed); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } function choose(rand, items) { return items[Math.floor(rand() * items.length)]; } function randomInt(rand, min, max) { return min + Math.floor(rand() * (max - min + 1)); } function normalizeExpression(raw) { return raw .replace(/\s/g, "") .replace(/x/gi, "×") .replace(/\*/g, "×") .replace(/\//g, "÷") .replace(/[–—]/g, "-"); } function toJsExpression(expr) { return expr.replace(/×/g, "*").replace(/÷/g, "/"); } function safeEvalExpression(expr) { if (!/^[0-9+\-×÷]+$/.test(expr)) return null; if (/^[+×÷]/.test(expr) || /[+\-×÷]$/.test(expr)) return null; if (/[+\-×÷]{2,}/.test(expr)) return null; try { const value = Function(`"use strict"; return (${toJsExpression(expr)});`)(); if (!Number.isFinite(value)) return null; return value; } catch { return null; } } function isIntegerLike(value) { return Math.abs(value - Math.round(value)) < 0.0000001; } function isValidEquation(eq) { if ([...eq].some(c => !ALLOWED.includes(c))) return false; if ((eq.match(/=/g) || []).length !== 1) return false; const [left, right] = eq.split("="); if (!left || !right) return false; const leftValue = safeEvalExpression(left); const rightValue = safeEvalExpression(right); if (leftValue === null || rightValue === null) return false; return Math.abs(leftValue - rightValue) < 0.0000001; } function countOperators(leftExpr) { return (leftExpr.match(/[+\-×÷]/g) || []).length; } function interleaveExpression(numbers, ops) { let expr = String(numbers[0]); for (let i = 0; i < ops.length; i++) { expr += ops[i] + String(numbers[i + 1]); } return expr; } function buildPuzzle(mode) { const config = MODES[mode]; const dateKey = localDateKey(); const seed = hashString(`${APP_VERSION}-${dateKey}-${mode}`); const rand = seededRandom(seed); for (let attempt = 0; attempt < 50000; attempt++) { const opCount = choose(rand, config.operatorCounts); const numbers = []; for (let i = 0; i < opCount + 1; i++) { const forceSmall = mode === "easy" && attempt < 1600; const max = forceSmall ? 20 : config.maxNumber; numbers.push(randomInt(rand, 1, max)); } const ops = []; for (let i = 0; i < opCount; i++) { ops.push(choose(rand, ["+", "-", "×", "÷"])); } const left = interleaveExpression(numbers, ops); const value = safeEvalExpression(left); if (value === null) continue; if (!isIntegerLike(value)) continue; const rounded = Math.round(value); if (rounded < 0) continue; if (Math.abs(rounded) > config.targetMaxAbs) continue; const answer = `${left}=${rounded}`; if (answer.length < config.minLength || answer.length > config.maxLength) continue; if (!isValidEquation(answer)) continue; if (countOperators(left) !== opCount) continue; return { dateKey, mode, answer, length: answer.length, maxGuesses: config.guesses }; } throw new Error("Unable to generate today's puzzle."); } function stateKey(mode) { return `${APP_VERSION}:state:${localDateKey()}:${mode}`; } function newState() { return { guesses: [], completed: false, won: false }; } function loadState(mode) { try { const raw = localStorage.getItem(stateKey(mode)); if (!raw) return newState(); const parsed = JSON.parse(raw); if (!Array.isArray(parsed.guesses)) return newState(); return { guesses: parsed.guesses.filter(g => typeof g === "string"), completed: Boolean(parsed.completed), won: Boolean(parsed.won) }; } catch { return newState(); } } function saveState() { localStorage.setItem(stateKey(currentMode), JSON.stringify(state)); } function cleanupOldDailyState() { const today = localDateKey(); const lastSeen = localStorage.getItem(LAST_SEEN_KEY); if (lastSeen && lastSeen !== today) { for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith(`${APP_VERSION}:state:`) && !key.includes(`:state:${today}:`)) { localStorage.removeItem(key); } } } localStorage.setItem(LAST_SEEN_KEY, today); } function markPlayedToday() { localStorage.setItem(LAST_PLAYED_KEY, localDateKey()); renderSplashProgress(); } function isNewDayForUser() { const lastPlayed = localStorage.getItem(LAST_PLAYED_KEY); const today = localDateKey(); return !lastPlayed || lastPlayed < today; } function resetCurrentGame() { state = newState(); entry = ""; localStorage.removeItem(stateKey(currentMode)); setMessage("Puzzle reset.", ""); renderGame(); renderSplashProgress(); } function scoreGuess(guess, answer) { const result = Array(answer.length).fill("absent"); const remaining = {}; for (let i = 0; i < answer.length; i++) { if (guess[i] === answer[i]) { result[i] = "correct"; } else { remaining[answer[i]] = (remaining[answer[i]] || 0) + 1; } } for (let i = 0; i < answer.length; i++) { if (result[i] === "correct") continue; const char = guess[i]; if (remaining[char] > 0) { result[i] = "present"; remaining[char]--; } } return result; } function keyboardRanks() { const rank = {}; const scoreValue = { absent: 1, present: 2, correct: 3 }; for (const guess of state.guesses) { const score = scoreGuess(guess, puzzle.answer); for (let i = 0; i < guess.length; i++) { const char = guess[i]; const current = rank[char]; const next = score[i]; if (!current || scoreValue[next] > scoreValue[current]) { rank[char] = next; } } } return rank; } function renderSplash() { splashDate.textContent = displayDate(); newDayLabel.textContent = isNewDayForUser() ? "New daily puzzles are ready." : "You have already played today."; newDayLabel.classList.toggle("is-new", isNewDayForUser()); renderThemeButton(); renderSplashProgress(); renderExampleBoard(); } function renderSplashProgress() { for (const mode of Object.keys(MODES)) { const progress = document.getElementById(`${mode}Progress`); const card = document.querySelector(`[data-start-mode="${mode}"]`); const pill = card && card.querySelector(".play-pill"); const s = loadState(mode); card.classList.remove("is-solved", "is-failed"); if (s.completed && s.won) { progress.textContent = `Solved ${s.guesses.length}/${MODES[mode].guesses}`; card.classList.add("is-solved"); if (pill) pill.textContent = "✓ Done"; } else if (s.completed) { progress.textContent = `Finished X/${MODES[mode].guesses}`; card.classList.add("is-failed"); if (pill) pill.textContent = "Done"; } else if (s.guesses.length > 0) { progress.textContent = `Resume ${s.guesses.length}/${MODES[mode].guesses}`; if (pill) pill.textContent = "Resume"; } else { progress.textContent = "Start today’s puzzle"; if (pill) pill.textContent = "Play"; } } } function renderExampleBoard() { const answer = "6+3=9"; const guesses = ["1+2=3", "4+5=9", "6+3=9"]; exampleBoard.innerHTML = ""; for (const guess of guesses) { const row = document.createElement("div"); row.className = "example-row"; const score = scoreGuess(guess, answer); for (let i = 0; i < guess.length; i++) { const tile = document.createElement("div"); tile.className = `tile ${score[i]}`; tile.textContent = guess[i]; row.appendChild(tile); } exampleBoard.appendChild(row); } } function openGame(mode) { let nextPuzzle; try { nextPuzzle = buildPuzzle(mode); } catch (err) { console.error(err); newDayLabel.textContent = "Sorry, today's puzzle could not be generated. Please try again."; return; } currentMode = mode; puzzle = nextPuzzle; state = loadState(mode); entry = ""; splashView.classList.remove("active"); gameView.classList.add("active"); setMessage(""); renderGame(); } function closeGame() { gameView.classList.remove("active"); splashView.classList.add("active"); renderSplash(); } function renderGame() { document.documentElement.style.setProperty("--cols", puzzle.length); gameTitle.innerHTML = `quadle · ${MODES[currentMode].label}`; dateLabel.textContent = puzzle.dateKey; lengthLabel.textContent = `${puzzle.length} chars`; guessLabel.textContent = `${state.guesses.length}/${puzzle.maxGuesses}`; renderThemeButton(); renderBoard(); renderAnswer(); renderEntry(); renderCalculator(); } function renderBoard() { board.innerHTML = ""; const activeIndex = state.completed ? Math.min(state.guesses.length - 1, puzzle.maxGuesses - 1) : Math.min(state.guesses.length, puzzle.maxGuesses - 1); for (let r = 0; r < puzzle.maxGuesses; r++) { const row = document.createElement("div"); row.className = r === activeIndex ? "row active-row" : "row"; row.dataset.rowIndex = String(r); const guess = state.guesses[r]; const score = guess ? scoreGuess(guess, puzzle.answer) : null; for (let c = 0; c < puzzle.length; c++) { const tile = document.createElement("div"); tile.className = "tile"; if (guess) { tile.textContent = guess[c]; tile.classList.add(score[c]); } row.appendChild(tile); } board.appendChild(row); } gameScroll.scrollTop = 0; } function renderAnswer() { answerSection.classList.toggle("visible", state.completed); answerRow.innerHTML = ""; answerRow.className = `answer-row ${state.won ? "win" : "loss"}`; if (!state.completed) return; for (const char of puzzle.answer) { const tile = document.createElement("div"); tile.className = "tile"; tile.textContent = char; answerRow.appendChild(tile); } } function renderEntry() { entryGuessBtn.disabled = state.completed; entryRow.innerHTML = ""; for (let i = 0; i < puzzle.length; i++) { const tile = document.createElement("div"); tile.className = "entry-tile"; const char = entry[i] || ""; if (char) { tile.textContent = char; tile.classList.add("filled"); } if (!state.completed && i === entry.length) { tile.classList.add("cursor"); } entryRow.appendChild(tile); } } function renderCalculator() { const ranks = keyboardRanks(); calculator.innerHTML = ""; for (const key of CALC_KEYS) { const button = document.createElement("button"); button.type = "button"; button.textContent = key.label; button.className = `calc-btn ${key.type}`; if (key.span === 3 && key.label === "0") button.classList.add("wide"); if (key.span === 3 && key.label === "⌫") button.classList.add("delete-wide"); if (ranks[key.label] && !["control", "danger", "submit"].includes(key.type)) { button.classList.add(`known-${ranks[key.label]}`); } button.disabled = state.completed; button.addEventListener("click", () => handleKey(key.label)); calculator.appendChild(button); } } function setMessage(text, type = "") { message.textContent = text; message.className = "message " + type; } function handleKey(key) { if (state.completed) return; if (key === "⌫") { entry = entry.slice(0, -1); setMessage(""); renderEntry(); return; } if (key === "Guess") { submitGuess(); return; } if (entry.length >= puzzle.length) { setMessage(`Today's answer is ${puzzle.length} characters.`, "bad"); return; } entry = normalizeExpression(entry + key); setMessage(""); renderEntry(); } function submitGuess() { if (state.completed) return; const guess = normalizeExpression(entry); if (guess.length !== puzzle.length) { setMessage(`Fill all ${puzzle.length} boxes before guessing.`, "bad"); return; } if (!isValidEquation(guess)) { setMessage("That is not a true equation.", "bad"); return; } state.guesses.push(guess); markPlayedToday(); if (guess === puzzle.answer) { state.completed = true; state.won = true; setMessage("Correct. Nice solve.", "good"); } else if (state.guesses.length >= puzzle.maxGuesses) { state.completed = true; state.won = false; setMessage("Out of guesses. The answer is revealed above the keypad.", "bad"); } else { const left = puzzle.maxGuesses - state.guesses.length; setMessage(`${left} ${left === 1 ? "guess" : "guesses"} left.`); } entry = ""; saveState(); renderGame(); } function applyInitialTheme() { const saved = localStorage.getItem(THEME_KEY); const preferred = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; setTheme(saved || preferred); } function setTheme(theme) { const safe = theme === "light" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", safe); localStorage.setItem(THEME_KEY, safe); const themeColorMeta = document.getElementById("themeColorMeta"); if (themeColorMeta) { themeColorMeta.setAttribute("content", safe === "light" ? "#f6f8fb" : "#0d1117"); } renderThemeButton(); } function toggleTheme() { const current = document.documentElement.getAttribute("data-theme") || "dark"; setTheme(current === "dark" ? "light" : "dark"); } function renderThemeButton() { const current = document.documentElement.getAttribute("data-theme") || "dark"; themeToggle.textContent = current === "dark" ? "☾" : "☀"; themeToggle.setAttribute("aria-label", current === "dark" ? "Switch to light mode" : "Switch to dark mode"); } document.querySelectorAll("[data-start-mode]").forEach(button => { button.addEventListener("click", () => openGame(button.dataset.startMode)); }); themeToggle.addEventListener("click", toggleTheme); backBtn.addEventListener("click", closeGame); resetBtn.addEventListener("click", resetCurrentGame); entryGuessBtn.addEventListener("click", submitGuess); document.addEventListener("keydown", handlePhysicalKey); function handlePhysicalKey(event) { if (!gameView.classList.contains("active")) return; if (event.metaKey || event.ctrlKey || event.altKey) return; const key = event.key; if (key === "Enter") { event.preventDefault(); submitGuess(); return; } if (key === "Backspace" || key === "Delete") { event.preventDefault(); handleKey("⌫"); return; } let mapped = null; if (/^[0-9]$/.test(key)) mapped = key; else if (key === "+") mapped = "+"; else if (key === "-") mapped = "-"; else if (key === "*" || key === "x" || key === "X") mapped = "×"; else if (key === "/") mapped = "÷"; else if (key === "=") mapped = "="; if (mapped) { event.preventDefault(); handleKey(mapped); } } })();