Back to Carrot Paws

Carrot Pawdle

Guess the cosy 5-letter dog word in 6 tries.

Tap the board, then use your keyboard to type. Press Enter to guess.

Quick guide

How To Play

Guess the cosy 5-letter dog word. Each tile tells you how close your guess was.

Green Right letter, right spot
Orange Right letter, wrong spot
Gray Letter is not in the word
Back to Carrot Paws
(function(){ const SUPABASE_URL = "https://uhhntlbgcxqmukorpcnk.supabase.co"; const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVoaG50bGJnY3hxbXVrb3JwY25rIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg0ODk5NzUsImV4cCI6MjA5NDA2NTk3NX0.Q3jaRA4tVuU6l670Gjth1IMiMW3xdwnvculOQiJCa_4"; const supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY); const words = ["WALKS","LEASH","BARKS","WOOFS","FETCH","SNIFF","TRAIL","HAPPY","TAILS","BONES","PUPPY","FIELD","STICK","TREAT","PATCH","COUCH","BLANK","SNUGS","CUDDY","GROOM","PARKS","ROUND","NAPPY","BRUSH","BAGGY","SOCKS","CRATE","GRASS","RIVER","PORCH","COATS","CHARM","LIGHT","BERRY","CLOUD","DREAM","SLEEP","MUDDY","EAGER","LOYAL","SCENT","HOWLS","CHEWS","HOUND","CORGI","DINGO","DOGGY","FURRY","BELLY","ROVER","SCOUT","SNOUT","CLAWS","TUMMY","BUDDY","FUZZY","BRAVE","GUARD","TRACK","CHASE","GREET","JUMPS","SWIMS","LICKS","ROWDY","PLUSH","WATER","BEACH","AGILE","ROAMS","BOWLS","LEAPS","SOGGY","PERKY","PEPPY","JOLLY","MERRY","SILLY","SLEEK","SWIFT","CHILL","HIKES","SMELL","LEGGY","ZIPPY","PUDGY","DITSY","MUGGY","DUSKY","MISTY","SUNNY","LODGE","SHADY","CRISP","BRISK","BALMY","SMOKY","MOSSY","LEAFY"]; const startDate = Date.UTC(2026, 0, 1); const todayUTC = Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate()); const dayNumber = Math.floor((todayUTC - startDate) / 86400000); const answer = words[((dayNumber % words.length) + words.length) % words.length]; const grid = document.getElementById("pawdleGrid"); const input = document.getElementById("pawdleInput"); const form = document.getElementById("pawdleForm"); const message = document.getElementById("pawdleMessage"); const clearBtn = document.getElementById("pawdleNewGame"); const shareBtn = document.getElementById("pawdleShare"); const leaderboard = document.getElementById("pawdleLeaderboard"); const puzzleDate = new Date().toISOString().split("T")[0]; let currentRow = 0, currentGuess = "", gameOver = false, results = [], scoreSubmitted = false; // Detect the real phone keyboard and switch to a compact play layout. // iOS changes visualViewport.height when the native keyboard opens. function setPawdleViewportHeight(){ const vv = window.visualViewport; const height = vv ? vv.height : window.innerHeight; const offsetTop = vv ? vv.offsetTop : 0; document.documentElement.style.setProperty("--pawdle-vvh", height + "px"); document.documentElement.style.setProperty("--pawdle-vv-top", offsetTop + "px"); } function isPawdleMobileKeyboardDevice(){ // Keep all keyboard/scroll locking off desktop, including narrow desktop windows // and touchscreen laptops. Only real phone/tablet browsers get this path. return window.matchMedia("(max-width: 700px)").matches && /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); } let pawdleScrollFrame = null; let pawdleLastRow = -1; function forceActiveRowIntoView(){ if (gameOver) return; if (!isPawdleMobileKeyboardDevice()) return; const vv = window.visualViewport; const keyboardOpen = document.body.classList.contains("pawdle-keyboard-open") || (vv && (window.innerHeight - vv.height > 120)); if (!keyboardOpen) return; const rowIndex = Math.min(currentRow, 5); const row = document.querySelector(`[data-row="${rowIndex}"]`)?.closest('.pawdle-row'); if (!row) return; if (pawdleScrollFrame) cancelAnimationFrame(pawdleScrollFrame); pawdleScrollFrame = requestAnimationFrame(() => { const viewportHeight = vv ? vv.height : window.innerHeight; const rect = row.getBoundingClientRect(); const safeTop = 88; const safeBottom = viewportHeight - 210; let targetScroll = null; if (rect.bottom > safeBottom) { targetScroll = window.scrollY + (rect.bottom - safeBottom); } else if (rect.top < safeTop) { targetScroll = window.scrollY - (safeTop - rect.top); } if (targetScroll !== null) { window.scrollTo({ top: Math.max(0, targetScroll), behavior: rowIndex === pawdleLastRow ? 'auto' : 'smooth' }); } pawdleLastRow = rowIndex; }); } function syncKeyboardOpenClass(){ setPawdleViewportHeight(); const vv = window.visualViewport; const isMobileKeyboardDevice = isPawdleMobileKeyboardDevice(); const keyboardOpen = isMobileKeyboardDevice && ( document.activeElement === input || (vv && (window.innerHeight - vv.height > 140)) ); const wasOpen = document.body.classList.contains("pawdle-keyboard-open"); document.body.classList.toggle("pawdle-keyboard-open", Boolean(keyboardOpen && !gameOver)); updateLateRowClass(); if (keyboardOpen) { clearTimeout(window.__pawdleKeyboardTimer); window.__pawdleKeyboardTimer = setTimeout(forceActiveRowIntoView, 140); } // Scroll row 1 into view when keyboard first opens if (keyboardOpen && !wasOpen && currentRow === 0) { setTimeout(() => { const firstRow = document.querySelector('[data-row="0"]')?.closest('.pawdle-row'); if (firstRow) firstRow.scrollIntoView({ block: 'start', behavior: 'smooth' }); }, 150); } } function updateLateRowClass(){ document.body.classList.remove("pawdle-row-0", "pawdle-row-1", "pawdle-row-2", "pawdle-row-3", "pawdle-row-4", "pawdle-row-5"); const mobile = isPawdleMobileKeyboardDevice() && !gameOver; if (mobile) document.body.classList.add("pawdle-row-" + Math.min(currentRow, 5)); const late = mobile && currentRow >= 3; document.body.classList.toggle("pawdle-late-row", Boolean(late)); } setPawdleViewportHeight(); window.addEventListener("resize", syncKeyboardOpenClass, { passive: true }); if (window.visualViewport) { window.visualViewport.addEventListener("resize", syncKeyboardOpenClass, { passive: true }); } // ── localStorage keys ──────────────────────────────────────────────── const LS_NAME = "pawdle_player_name"; const LS_STATE = "pawdle_state_" + puzzleDate; // ── Save/restore today's game state ────────────────────────────────── function saveState() { const state = { currentRow, results, gameOver, scoreSubmitted }; try { localStorage.setItem(LS_STATE, JSON.stringify(state)); } catch(e) {} } function restoreState() { try { const raw = localStorage.getItem(LS_STATE); if (!raw) return false; const state = JSON.parse(raw); currentRow = state.currentRow || 0; results = state.results || []; gameOver = state.gameOver || false; scoreSubmitted = state.scoreSubmitted || false; return true; } catch(e) { return false; } } function restoreGrid() { // Replay saved results back onto the grid tiles results.forEach((result, rowIndex) => { const guessLetters = getGuessLettersFromResult(rowIndex); result.forEach((state, col) => { const tile = document.querySelector(`[data-row="${rowIndex}"][data-col="${col}"]`); if (tile) { tile.textContent = guessLetters[col] || ""; tile.classList.add(state); if (guessLetters[col]) tile.classList.add("filled"); } }); updateKeyboard(guessLetters.join(""), result); }); } // We don't store the actual guesses, only results - reconstruct letters from answer + result // Actually store guesses too for restore let guessHistory = []; function saveGuessHistory() { try { localStorage.setItem(LS_STATE + "_guesses", JSON.stringify(guessHistory)); } catch(e) {} } function restoreGuessHistory() { try { const raw = localStorage.getItem(LS_STATE + "_guesses"); return raw ? JSON.parse(raw) : []; } catch(e) { return []; } } function getGuessLettersFromResult(rowIndex) { return (guessHistory[rowIndex] || "").split(""); } function buildGrid(){ grid.innerHTML = ""; for(let r=0;r<6;r++){ const row = document.createElement("div"); row.className = "pawdle-row"; for(let c=0;c<5;c++){ const tile = document.createElement("div"); tile.className = "pawdle-tile"; tile.dataset.row = r; tile.dataset.col = c; row.appendChild(tile); } grid.appendChild(row); } } function settlePawdleView(){ // Keep iOS/Brave steady while typing: no scripted page scrolling after Enter. // Desktop uses its own gentle row-follow below. return; } function followActiveRowOnDesktop(){ if (gameOver) return; if (isPawdleMobileKeyboardDevice()) return; if (currentRow < 3) return; // start following from visible row 4 const row = document.querySelector(`[data-row="${currentRow}"]`)?.closest(".pawdle-row"); if (!row) return; setTimeout(() => { try { row.scrollIntoView({ block: "center", behavior: "smooth" }); } catch(e) { row.scrollIntoView(false); } }, 40); } function releasePawdleKeyboard(){ try { input.blur(); } catch(e) {} document.body.classList.remove("pawdle-keyboard-open"); document.body.classList.remove("pawdle-late-row", "pawdle-row-0", "pawdle-row-1", "pawdle-row-2", "pawdle-row-3", "pawdle-row-4", "pawdle-row-5"); document.body.classList.add("pawdle-game-ended"); setPawdleViewportHeight(); } function focusInput(){ if (gameOver) return; try { input.focus({ preventScroll: true }); } catch(e) { input.focus(); } } input.addEventListener("blur", () => { if (gameOver) releasePawdleKeyboard(); else setTimeout(syncKeyboardOpenClass, 80); }); function handleKey(key){ try { ensureMusic(); } catch(e) {} if(gameOver) return; message.textContent = ""; if(key === "ENTER") return submitGuess(); if(key === "⌫" || key === "BACKSPACE"){ currentGuess = currentGuess.slice(0,-1); updateCurrentRow(); sfxDelete(); return; } if(/^[A-Z]$/.test(key) && currentGuess.length < 5){ currentGuess += key; updateCurrentRow(); sfxTap(); } } function updateCurrentRow(){ updateLateRowClass(); followActiveRowOnDesktop(); forceActiveRowIntoView(); // clear all cursor highlights first document.querySelectorAll('.pawdle-tile.active-cursor').forEach(t => t.classList.remove('active-cursor')); for(let c=0;c<5;c++){ const tile = document.querySelector(`[data-row="${currentRow}"][data-col="${c}"]`); tile.textContent = currentGuess[c] || ""; tile.classList.toggle("filled", Boolean(currentGuess[c])); } // highlight cursor position - clamp to col 4 when row is full so backspace hint stays visible if (!gameOver) { const cursorCol = Math.min(currentGuess.length, 4); const cursor = document.querySelector(`[data-row="${currentRow}"][data-col="${cursorCol}"]`); if (cursor) cursor.classList.add('active-cursor'); } } function scoreGuess(guess){ const result = Array(5).fill("absent"); const answerLetters = answer.split(""); for(let i=0;i<5;i++){ if(guess[i] === answer[i]){ result[i] = "correct"; answerLetters[i] = null; } } for(let i=0;i<5;i++){ if(result[i] === "correct") continue; const index = answerLetters.indexOf(guess[i]); if(index !== -1){ result[i] = "present"; answerLetters[index] = null; } } return result; } function updateKeyboard(guess, result){ guess.split("").forEach((letter, index) => { const key = document.querySelector(`.pawdle-key[data-key="${letter}"]`); if(!key) return; const state = result[index]; if(state === "correct"){ key.classList.remove("present", "absent"); key.classList.add("correct"); } else if(state === "present"){ if(!key.classList.contains("correct")){ key.classList.remove("absent"); key.classList.add("present"); } } else if(state === "absent"){ if(!key.classList.contains("correct") && !key.classList.contains("present")){ key.classList.add("absent"); } } }); } async function loadLeaderboard(){ leaderboard.innerHTML = '
Loading leaderboard...
'; const { data, error } = await supabaseClient .from("pawdle_scores") .select("player_name, guesses, created_at") .limit(1000); if(error){ console.error(error); leaderboard.innerHTML = '
Leaderboard error
'; return; } if(!data || data.length === 0){ leaderboard.innerHTML = '
No scores yet. Be the first pup on the board!
'; return; } const totals = {}; data.forEach(score => { const name = String(score.player_name || "").trim(); if(!name) return; const key = name.toLowerCase(); const pointsMap = {1:20,2:15,3:11,4:8,5:5,6:3}; const points = pointsMap[Number(score.guesses || 6)] || 3; if(!totals[key]){ totals[key] = { name, points: 0, wins: 0, best: 6, latest: score.created_at }; } totals[key].points += points; totals[key].wins += 1; totals[key].best = Math.min(totals[key].best, Number(score.guesses || 6)); totals[key].latest = score.created_at > totals[key].latest ? score.created_at : totals[key].latest; }); const leaders = Object.values(totals) .sort((a, b) => b.points - a.points || b.wins - a.wins || a.best - b.best || new Date(b.latest) - new Date(a.latest)); let expanded = false; const avatarData = [ { medal: "πŸ₯‡", src: "images/pawdle-archie.webp", cls: "archie", alt: "Archie" }, { medal: "πŸ₯ˆ", src: "images/pawdle-pearl.webp", cls: "pearl", alt: "Pearl" }, { medal: "πŸ₯‰", src: "images/pawdle-teddy.webp", cls: "teddy", alt: "Teddy" } ]; function drawLeaderboard(){ const visible = expanded ? leaders.slice(0, 10) : leaders.slice(0, 3); leaderboard.innerHTML = visible.map((score, index) => { const topAvatar = avatarData[index]; const avatarHtml = topAvatar ? `${topAvatar.alt}` : `${escapeHtml(score.name.charAt(0).toUpperCase())}`; const rankHtml = topAvatar ? topAvatar.medal : `#${index + 1}`; return `
${rankHtml} ${avatarHtml} ${escapeHtml(score.name)} ${score.points} pts
`; }).join(""); if(leaders.length > 3){ const button = document.createElement("button"); button.className = "leaderboard-toggle"; button.type = "button"; button.textContent = expanded ? "Show Top 3" : "View Full Leaderboard"; button.addEventListener("click", () => { expanded = !expanded; drawLeaderboard(); }); leaderboard.appendChild(button); } } drawLeaderboard(); } function escapeHtml(text){ return String(text).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); } function showNameModal(savedName) { return new Promise((resolve) => { const modal = document.getElementById("pawdleModal"); const input = document.getElementById("pawdleModalInput"); const submitBtn = document.getElementById("pawdleModalSubmit"); const skipBtn = document.getElementById("pawdleModalSkip"); input.value = savedName || ""; modal.hidden = false; // Focus after a brief delay so iOS doesn't fight the keyboard setTimeout(() => { try { input.focus(); } catch(e) {} }, 80); function finish(name) { modal.hidden = true; submitBtn.removeEventListener("click", onSubmit); skipBtn.removeEventListener("click", onSkip); input.removeEventListener("keydown", onKey); resolve(name); } function onSubmit() { finish(input.value.trim().slice(0, 20)); } function onSkip() { finish(null); } function onKey(e) { if (e.key === "Enter") onSubmit(); } submitBtn.addEventListener("click", onSubmit); skipBtn.addEventListener("click", onSkip); input.addEventListener("keydown", onKey); }); } async function submitScore(guesses){ if(scoreSubmitted) return; const savedName = (() => { try { return localStorage.getItem(LS_NAME) || ""; } catch(e) { return ""; } })(); const playerName = await showNameModal(savedName); if(!playerName) return; const cleanName = playerName.trim().slice(0, 20); if(!cleanName) return; try { localStorage.setItem(LS_NAME, cleanName); } catch(e) {} scoreSubmitted = true; saveState(); const { error } = await supabaseClient .from("pawdle_scores") .insert([{ player_name: cleanName, guesses, puzzle_date: puzzleDate }]); if(error){ message.textContent = "You won! Leaderboard save did not work this time"; scoreSubmitted = false; saveState(); return; } message.textContent = "Teddy is proud of you - your points were added"; loadLeaderboard(); } async function isValidWord(word) { try { const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word.toLowerCase()}`); return res.ok; } catch(e) { return true; // if offline/API fails, allow the word through } } // Shake animation for invalid word function clearCursor() { document.querySelectorAll('.pawdle-tile.active-cursor').forEach(t => t.classList.remove('active-cursor')); } function shakeCurrentRow() { const row = document.querySelectorAll(`[data-row="${currentRow}"]`); row.forEach(tile => { tile.classList.add('shake'); setTimeout(() => tile.classList.remove('shake'), 500); }); } async function submitGuess(){ if(currentGuess.length !== 5){ message.textContent = "Type 5 letters first 🐢"; return; } // Check for duplicate guess if (guessHistory.includes(currentGuess)) { message.textContent = "You already tried that word!"; shakeCurrentRow(); return; } // Validate word message.textContent = "Checking..."; const valid = await isValidWord(currentGuess); if (!valid) { message.textContent = "That's not a word! Try again 🐢"; shakeCurrentRow(); return; } message.textContent = ""; const result = scoreGuess(currentGuess); updateKeyboard(currentGuess, result); results.push(result); const revealedGuess = currentGuess; const submittedRow = currentRow; guessHistory.push(currentGuess); saveGuessHistory(); // Save immediately so the final row cannot be bypassed by refreshing during the reveal animation. // If this is row 6, lock the loss state before the timeout finishes. if (submittedRow === 5 && revealedGuess !== answer) { currentRow = 6; gameOver = true; saveState(); } else { saveState(); } // Schedule all audio immediately via Web Audio timing (avoids iOS setTimeout restriction) result.forEach((state, c) => { const audioDelay = c * 0.13; if (state === "correct") sfxCorrect(audioDelay); else if (state === "present") sfxPresent(audioDelay); else sfxAbsent(audioDelay); }); // Visual tile flip still staggered with setTimeout result.forEach((state, c) => { setTimeout(() => { const tile = document.querySelector(`[data-row="${submittedRow}"][data-col="${c}"]`); if (tile) tile.classList.add(state); }, c * 120); }); const revealDone = result.length * 120 + 80; setTimeout(async () => { if(revealedGuess === answer){ message.textContent = "Teddy is proud of you"; gameOver = true; shareBtn.hidden = false; sfxWin(); clearCursor(); releasePawdleKeyboard(); saveState(); await submitScore(submittedRow + 1); setTimeout(() => { try { leaderboard.scrollIntoView({ block: "center", behavior: "smooth" }); } catch(e) {} }, 250); return; } if(submittedRow === 5){ currentRow = 6; currentGuess = ""; message.innerHTML = `Don't cry, the word was ${answer}.
Teddy says try again tomorrow. 🐾`; gameOver = true; shareBtn.hidden = false; sfxLose(); clearCursor(); releasePawdleKeyboard(); saveState(); return; } currentRow = submittedRow + 1; currentGuess = ""; updateLateRowClass(); followActiveRowOnDesktop(); updateCurrentRow(); requestAnimationFrame(() => { updateLateRowClass(); setPawdleViewportHeight(); followActiveRowOnDesktop(); [0, 120, 260].forEach(delay => { setTimeout(forceActiveRowIntoView, delay); }); }); saveState(); }, revealDone); } function shareResult(){ const map = {correct:"🟩", present:"🟧", absent:"⬜"}; const score = results.some(r => r.every(x => x === "correct")) ? results.length : "X"; const text = `Carrot Pawdle ${score}/6\n\n` + results.map(row => row.map(x => map[x]).join("")).join("\n") + "\n\ncarrotpaws.com/carrot-pawdle.html"; if(navigator.share){ navigator.share({text}).catch(()=>{}); } else { navigator.clipboard.writeText(text); message.textContent = "Result copied"; } } clearBtn.addEventListener("click", () => { if (gameOver) return; currentGuess = ""; message.textContent = ""; updateCurrentRow(); }); shareBtn.addEventListener("click", shareResult); // Track whether a submit is already in-flight to prevent double-submit // (mobile keyboards can fire both "form submit" and "keydown Enter" at once) let submitPending = false; function safeSubmit() { if (submitPending) return; submitPending = true; submitGuess(); input.value = ""; if (!isPawdleMobileKeyboardDevice()) focusInput(); setTimeout(() => { submitPending = false; }, 300); } // Input event: sole source of letter input on mobile (avoids keydown duplication) input.addEventListener("input", event => { // Catch Android backspace (fires as inputType deleteContentBackward, not keydown) if (event.inputType === "deleteContentBackward" || event.inputType === "deleteWordBackward") { event.target.value = ""; handleKey("BACKSPACE"); haptic(); return; } const raw = event.target.value; event.target.value = ""; const letters = raw.toUpperCase().replace(/[^A-Z]/g, "").split("").filter(Boolean); letters.forEach(letter => { handleKey(letter); haptic(); }); }); // Form submit: fired by mobile "Go" key (enterkeyhint="go") and desktop Enter form.addEventListener("submit", event => { event.preventDefault(); safeSubmit(); }); // Keydown: ENTER + BACKSPACE only, with guard to avoid double-processing letters // Uses capture:false so it doesn't fire before the input event on mobile document.addEventListener("keydown", event => { const key = event.key.toUpperCase(); if (key === "ENTER") { // Only handle if not already handled by form submit if (document.activeElement !== input) { event.preventDefault(); safeSubmit(); } return; } if (key === "BACKSPACE") { event.preventDefault(); handleKey("BACKSPACE"); input.value = ""; return; } // Desktop physical keyboard: only when NOT typing into the input if (document.activeElement !== input && /^[A-Z]$/.test(key)) { handleKey(key); } }); document.querySelectorAll(".pawdle-key").forEach(key => { key.addEventListener("touchstart", event => { // Prevent iOS from moving focus to the button and causing another layout jump. event.preventDefault(); }, { passive: false }); key.addEventListener("click", event => { event.preventDefault(); handleKey(key.dataset.key); input.value = ""; focusInput(); haptic(); }); }); grid.addEventListener("click", focusInput); grid.addEventListener("touchstart", (e) => { focusInput(e); ensureMusic(); }, { passive: true }); document.querySelector(".pawdle-card").addEventListener("click", focusInput); // Haptic feedback - works on Android; silently ignored on iOS function haptic(duration) { try { navigator.vibrate && navigator.vibrate(duration || 18); } catch(e) {} } // ── AUDIO ENGINE ───────────────────────────────────────────────────────── let audioCtx = null; let sfxGain = null; let musicGain = null; let audioMuted = true; let musicPlaying = false; let musicTimeout = null; function getAudioCtx() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); sfxGain = audioCtx.createGain(); sfxGain.gain.value = 0.65; sfxGain.connect(audioCtx.destination); musicGain = audioCtx.createGain(); musicGain.gain.value = 0.18; musicGain.connect(audioCtx.destination); } return audioCtx; } function makeReverb(ctx) { const len = ctx.sampleRate * 2.2; const buf = ctx.createBuffer(2, len, ctx.sampleRate); for (let c = 0; c < 2; c++) { const d = buf.getChannelData(c); for (let i = 0; i < len; i++) d[i] = (Math.random()*2-1) * Math.pow(1-i/len, 3.0); } const conv = ctx.createConvolver(); conv.buffer = buf; return conv; } function playPianoNote(freq, startTime, duration, gain, ctx, output) { const osc1 = ctx.createOscillator(); const osc2 = ctx.createOscillator(); const g = ctx.createGain(); osc1.type = 'triangle'; osc1.frequency.setValueAtTime(freq, startTime); osc2.type = 'sine'; osc2.frequency.setValueAtTime(freq * 2.01, startTime); g.gain.setValueAtTime(0, startTime); g.gain.linearRampToValueAtTime(gain, startTime + 0.015); g.gain.exponentialRampToValueAtTime(gain * 0.4, startTime + duration * 0.3); g.gain.exponentialRampToValueAtTime(0.0001, startTime + duration); osc1.connect(g); osc2.connect(g); g.connect(output); osc1.start(startTime); osc2.start(startTime); osc1.stop(startTime + duration + 0.05); osc2.stop(startTime + duration + 0.05); } const MELODY = [ [261.63,0.0,0.5,0.7],[329.63,0.5,0.5,0.5],[392.00,1.0,0.5,0.5],[523.25,1.5,1.0,0.6], [440.00,2.5,0.5,0.5],[392.00,3.0,0.5,0.45],[329.63,3.5,1.0,0.55], [293.66,4.5,0.5,0.5],[261.63,5.0,0.5,0.6],[329.63,5.5,0.5,0.45],[392.00,6.0,1.5,0.55], [329.63,7.5,0.5,0.5],[293.66,8.0,0.5,0.45],[261.63,8.5,1.5,0.7], [523.25,10.0,0.5,0.45],[587.33,10.5,0.5,0.4],[659.25,11.0,1.0,0.5], [587.33,12.0,0.5,0.4],[523.25,12.5,0.5,0.45], [493.88,13.0,0.5,0.4],[440.00,13.5,1.0,0.5],[392.00,14.5,0.5,0.4], [440.00,15.0,0.5,0.45],[392.00,15.5,0.5,0.4],[329.63,16.0,1.0,0.5],[293.66,17.0,0.5,0.4], [261.63,17.5,2.5,0.65], ]; const BASS = [ [130.81,0.0,1.8,0.35],[146.83,2.0,1.8,0.3],[130.81,4.0,1.8,0.32],[146.83,6.0,1.8,0.3], [130.81,8.0,1.8,0.35],[164.81,10.0,1.8,0.3],[146.83,12.0,1.8,0.3], [130.81,14.0,1.8,0.32],[130.81,16.0,1.8,0.35],[130.81,18.0,1.8,0.3], ]; const MELODY_DURATION = 20.0; function scheduleMusicBar(startTime) { const ctx = getAudioCtx(); if (!scheduleMusicBar._reverb) { scheduleMusicBar._reverb = makeReverb(ctx); scheduleMusicBar._reverb.connect(musicGain); } const rev = scheduleMusicBar._reverb; MELODY.forEach(([freq,offset,dur,gain]) => playPianoNote(freq, startTime+offset, dur, gain, ctx, rev)); BASS.forEach(([freq,offset,dur,gain]) => playPianoNote(freq, startTime+offset, dur*1.2, gain*0.7, ctx, rev)); } function startMusic() { if (audioMuted || musicPlaying) return; musicPlaying = true; const ctx = getAudioCtx(); // Reconnect musicGain (was disconnected by stopMusic) and restore volume if (musicGain) { try { musicGain.disconnect(); } catch(e) {} musicGain.connect(ctx.destination); musicGain.gain.setValueAtTime(0.18, ctx.currentTime); } const loop = () => { if (!musicPlaying) return; scheduleMusicBar(ctx.currentTime + 0.1); musicTimeout = setTimeout(loop, (MELODY_DURATION - 0.3) * 1000); }; loop(); } function stopMusic() { musicPlaying = false; if (musicTimeout) clearTimeout(musicTimeout); musicTimeout = null; // Disconnect musicGain so any already-scheduled nodes produce no output if (musicGain) { try { musicGain.disconnect(); } catch(e) {} } } // ── Marimba/xylophone helper - warm wooden tone ────────────────────────── function marimba(freq, gainVal, startTime) { const ctx = getAudioCtx(); const t = startTime; // Primary tone - triangle for warmth const o1 = ctx.createOscillator(); o1.type = 'triangle'; o1.frequency.setValueAtTime(freq, t); const g1 = ctx.createGain(); g1.gain.setValueAtTime(0, t); g1.gain.linearRampToValueAtTime(gainVal, t + 0.006); g1.gain.exponentialRampToValueAtTime(gainVal * 0.3, t + 0.06); g1.gain.exponentialRampToValueAtTime(0.0001, t + 0.45); o1.connect(g1); g1.connect(sfxGain); o1.start(t); o1.stop(t + 0.5); // Overtone - adds woody brightness const o2 = ctx.createOscillator(); o2.type = 'sine'; o2.frequency.setValueAtTime(freq * 3.95, t); const g2 = ctx.createGain(); g2.gain.setValueAtTime(0, t); g2.gain.linearRampToValueAtTime(gainVal * 0.18, t + 0.005); g2.gain.exponentialRampToValueAtTime(0.0001, t + 0.08); o2.connect(g2); g2.connect(sfxGain); o2.start(t); o2.stop(t + 0.1); } function sfxTap() { if (audioMuted) return; const ctx = getAudioCtx(); if (ctx.state !== 'running') return; marimba(523.25, 0.22, ctx.currentTime + 0.01); // C5 - soft tap } function sfxDelete() { if (audioMuted) return; const ctx = getAudioCtx(); if (ctx.state !== 'running') return; marimba(392.00, 0.14, ctx.currentTime + 0.01); // G4 - softer, lower } function sfxCorrect(delay) { if (audioMuted) return; const ctx = getAudioCtx(); if (ctx.state !== 'running') return; marimba(659.25, 0.28, ctx.currentTime + 0.01 + (delay||0)); // E5 - bright happy } function sfxPresent(delay) { if (audioMuted) return; const ctx = getAudioCtx(); if (ctx.state !== 'running') return; marimba(587.33, 0.22, ctx.currentTime + 0.01 + (delay||0)); // D5 - mid, warm } function sfxAbsent(delay) { if (audioMuted) return; const ctx = getAudioCtx(); if (ctx.state !== 'running') return; marimba(392.00, 0.15, ctx.currentTime + 0.01 + (delay||0)); // G4 - low, soft } function sfxWin() { if (audioMuted) return; const ctx = getAudioCtx(); if (ctx.state !== 'running') return; const t = ctx.currentTime + 0.01; // Little ascending marimba run - C E G C marimba(523.25, 0.28, t); marimba(659.25, 0.28, t + 0.14); marimba(783.99, 0.28, t + 0.28); marimba(1046.50, 0.32, t + 0.42); } function sfxLose() { if (audioMuted) return; const ctx = getAudioCtx(); if (ctx.state !== 'running') return; const t = ctx.currentTime + 0.01; // Gentle descending - C G E C marimba(523.25, 0.2, t); marimba(392.00, 0.18, t + 0.2); marimba(329.63, 0.16, t + 0.4); marimba(261.63, 0.14, t + 0.6); } function ensureMusic() { return; } // ── Mute toggle button ─────────────────────────────────────────────────── const muteBtn = document.getElementById("pawdleMute"); if (muteBtn) { muteBtn.addEventListener("click", () => { audioMuted = !audioMuted; muteBtn.querySelector('.icon-music-on').style.display = audioMuted ? 'none' : ''; muteBtn.querySelector('.icon-music-off').style.display = audioMuted ? '' : 'none'; muteBtn.title = audioMuted ? "Turn music on" : "Turn music off"; muteBtn.setAttribute('aria-label', audioMuted ? "Unmute music" : "Mute music"); if (audioMuted) stopMusic(); else startMusic(); try { localStorage.setItem("pawdle_muted_v2", audioMuted ? "1" : "0"); } catch(e) {} }); try { if (localStorage.getItem("pawdle_muted_v2") === "0") { audioMuted = false; muteBtn.querySelector('.icon-music-on').style.display = 'none'; muteBtn.querySelector('.icon-music-off').style.display = ''; } } catch(e) {} } // Opening splash removed: start directly on the game. buildGrid(); // Restore today's game if the player has already started guessHistory = restoreGuessHistory(); if (restoreState()) { restoreGrid(); if (gameOver) { releasePawdleKeyboard(); clearCursor(); shareBtn.hidden = false; if (results.length > 0 && results[results.length - 1].every(r => r === "correct")) { message.textContent = "Teddy is proud of you - welcome back"; } else if (currentRow >= 6) { message.innerHTML = `Don't cry, the word was ${answer}.
Teddy says try again tomorrow. 🐾`; } } } // Now highlight cursor in correct position after state is restored if (!gameOver) updateCurrentRow(); loadLeaderboard(); window.scrollTo({ top: 0, left: 0, behavior: "instant" }); })();
Γ—