Skip to main content
Back to Carrot Paws
Carrot Pawdle
Guess the cosy 5-letter dog word in 6 tries.
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 β«
Tap the board, then use your keyboard to type. Press Enter to guess.
Share Result
P
A
W
S
Y
Green
Right letter, right spot
C
O
Z
Y
Y
Orange
Right letter, wrong spot
B
A
R
K
S
Gray
Letter is not in the word
Back to Carrot Paws
πΎ
You solved it!
Enter your name for the leaderboard
Add to leaderboard
Skip
Cookie notice
Carrot Paws only uses essential site functionality here. Third-party services such as Stripe, Patreon, Instagram, Facebook or TikTok may use their own cookies when you visit them.
(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
? ` `
: `${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" });
})();