"use strict"; // ========================= // ENGINE: THEMES + PUZZLES // ========================= // Base solved sudoku (values 0..8) const BASE_SOLUTION = [ [0,1,2,3,4,5,6,7,8], [3,4,5,6,7,8,0,1,2], [6,7,8,0,1,2,3,4,5], [1,2,3,4,5,6,7,8,0], [4,5,6,7,8,0,1,2,3], [7,8,0,1,2,3,4,5,6], [2,3,4,5,6,7,8,0,1], [5,6,7,8,0,1,2,3,4], [8,0,1,2,3,4,5,6,7] ]; // Normal difficulty givens const BASE_GIVEN_NORMAL = [ [null, null, null, 3, null, null, null, 7, null], [null, null, null, 6, null, null, 0, null, null], [null, null, 8, null, null, null, null, 4, null], [null, null, null, null, null, 6, null, null, 0 ], [null, 5, null, null, 8, null, null, 2, 3 ], [null, 8, null, 1, null, null, null, 5, 6 ], [null, null, 4, null, null, null, null, null, null], [null, null, null, null, 0, null, 2, 3, 4 ], [null, null, null, 2, null, null, null, null, null] ]; // Hard difficulty givens: fewer clues const BASE_GIVEN_HARD = [ [null, null, null, null, null, null, null, 7, null], [null, null, null, 6, null, null, 0, null, null], [null, null, 8, null, null, null, null, null, null], [null, null, null, null, null, 6, null, null, null], [null, 5, null, null, null, null, null, 2, null], [null, null, null, 1, null, null, null, null, 6 ], [null, null, 4, null, null, null, null, null, null], [null, null, null, null, 0, null, null, 3, 4 ], [null, null, null, 2, null, null, null, null, null] ]; const DIFFICULTIES = { normal: BASE_GIVEN_NORMAL, hard: BASE_GIVEN_HARD }; const THEME_SETS = [ { name: "Cozy Pets", emojis: ["🐼","🦊","🐰","🐧","🐸","🐯","🐨","🐥","🐻"] }, { name: "Forest Friends", emojis: ["🦌","🦉","🦝","🦔","🦊","🐿️","🐻","🐰","🐦"] }, { name: "Ocean Cuties", emojis: ["🐳","🦭","🦀","⭐","🐢","🐙","🐟","🐬","🪼"] } ]; function getDayOfYearFor(date) { const start = new Date(date.getFullYear(), 0, 0); return Math.floor((date - start) / 86400000); } function clone2D(a) { return a.map(r => r.slice()); } /** * makeDailyPuzzle(date, difficultyKey) * Returns: { theme, solution, puzzle } */ function makeDailyPuzzle(date, difficultyKey) { const dayIdx = getDayOfYearFor(date); const theme = THEME_SETS[dayIdx % THEME_SETS.length]; const shift = dayIdx % 9; const solution = BASE_SOLUTION.map(row => row.map(v => (v + shift) % 9) ); const baseGiven = DIFFICULTIES[difficultyKey] || BASE_GIVEN_NORMAL; const puzzle = baseGiven.map(row => row.map(v => v === null ? null : (v + shift) % 9) ); return { theme, solution, puzzle }; } // ========================= // UI STATE // ========================= let animals = []; // emoji array let currentSolution = []; // 9x9 numbers let currentPuzzle = []; // 9x9 numbers or null let selectedCell = null; let selectedAnimalIndex = null; let currentMode = "today-normal"; // "today-normal" | "today-hard" | "archive" let currentDate = new Date(); let currentDifficulty = "normal"; // "normal" | "hard" // DOM helpers const $ = sel => document.querySelector(sel); const $$ = sel => document.querySelectorAll(sel); // ========================= // UI RENDERING // ========================= function addEmojiToCell(cell, emoji, idx) { cell.innerHTML = ""; const span = document.createElement("div"); span.classList.add("emoji"); span.textContent = emoji; span.draggable = true; span.addEventListener("dragstart", e => e.dataTransfer.setData("text/plain", String(idx)) ); cell.appendChild(span); } function renderBoard() { const board = $("#board"); board.innerHTML = ""; for (let i = 0; i < 81; i++) { const r = Math.floor(i / 9); const c = i % 9; const cell = document.createElement("div"); cell.classList.add("cell"); cell.dataset.row = r; cell.dataset.col = c; if (c % 3 === 2) cell.classList.add("block-right"); if (r % 3 === 2) cell.classList.add("block-bottom"); const val = currentPuzzle[r][c]; if (val !== null) { cell.classList.add("given"); addEmojiToCell(cell, animals[val], val); } else { cell.addEventListener("click", () => selectCell(cell)); cell.addEventListener("dragover", e => { e.preventDefault(); cell.classList.add("drag-over"); }); cell.addEventListener("dragleave", () => cell.classList.remove("drag-over") ); cell.addEventListener("drop", e => { e.preventDefault(); cell.classList.remove("drag-over"); const idx = parseInt(e.dataTransfer.getData("text/plain")); place(r, c, idx, cell); }); } board.appendChild(cell); } } function loadPalette(theme) { animals = theme.emojis; const panel = $("#animals"); panel.innerHTML = ""; selectedAnimalIndex = null; animals.forEach((emoji, idx) => { const btn = document.createElement("div"); btn.classList.add("animal-choice"); btn.textContent = emoji; btn.draggable = true; btn.addEventListener("click", () => { $$(".animal-choice").forEach(b => b.classList.remove("selected")); btn.classList.add("selected"); selectedAnimalIndex = idx; // quick-place if a cell is already selected if (selectedCell && !selectedCell.classList.contains("given")) { const r = +selectedCell.dataset.row; const c = +selectedCell.dataset.col; place(r, c, idx, selectedCell); } }); btn.addEventListener("dragstart", e => e.dataTransfer.setData("text/plain", String(idx)) ); panel.appendChild(btn); }); } function selectCell(cell) { if (cell.classList.contains("given")) return; $$(".cell").forEach(c => c.classList.remove("selected")); cell.classList.add("selected"); selectedCell = cell; } // ========================= // SUDOKU RULES & ACTIONS // ========================= function isValid(idx, row, col) { // row for (let c = 0; c < 9; c++) { if (currentPuzzle[row][c] === idx) return false; } // col for (let r = 0; r < 9; r++) { if (currentPuzzle[r][col] === idx) return false; } // box const br = Math.floor(row / 3) * 3; const bc = Math.floor(col / 3) * 3; for (let r = br; r < br + 3; r++) { for (let c = bc; c < bc + 3; c++) { if (currentPuzzle[r][c] === idx) return false; } } return true; } function place(row, col, idx, cell) { if (idx == null || Number.isNaN(idx)) return; if (!isValid(idx, row, col)) { cell.classList.add("error"); setTimeout(() => cell.classList.remove("error"), 500); return; } currentPuzzle[row][col] = idx; addEmojiToCell(cell, animals[idx], idx); } function clearSelectedCell() { if (!selectedCell || selectedCell.classList.contains("given")) return; const r = +selectedCell.dataset.row; const c = +selectedCell.dataset.col; currentPuzzle[r][c] = null; selectedCell.innerHTML = ""; } function checkSolution() { let full = true; let wrong = false; $$(".cell").forEach(cell => { if (cell.classList.contains("given")) return; const r = +cell.dataset.row; const c = +cell.dataset.col; const val = currentPuzzle[r][c]; if (val == null) { full = false; return; } if (val !== currentSolution[r][c]) { wrong = true; cell.classList.add("error"); setTimeout(() => cell.classList.remove("error"), 500); } }); if (!full) { alert("Fill all squares first!"); } else if (wrong) { alert("Some emojis are in the wrong pen. Try again!"); } else { alert("🎉 You solved this PetDoku!"); } } function showSolution() { $$(".cell").forEach(cell => { if (cell.classList.contains("given")) return; const r = +cell.dataset.row; const c = +cell.dataset.col; const idx = currentSolution[r][c]; currentPuzzle[r][c] = idx; addEmojiToCell(cell, animals[idx], idx); }); } // ========================= // MODE / ARCHIVE / INIT // ========================= function updateLabels(theme, date, difficulty) { const themeLabel = $("#dailyTheme"); const dateLabel = $("#dateLabel"); const difficultyLabel = difficulty === "hard" ? "Hard" : "Normal"; themeLabel.textContent = `Theme: ${theme.name} • ${difficultyLabel}`; if (currentMode.startsWith("today")) { dateLabel.textContent = date.toLocaleDateString(undefined, { weekday:"short", month:"short", day:"numeric", year:"numeric" }); } else { dateLabel.textContent = `Archive: ${date.toLocaleDateString(undefined, { weekday:"short", month:"short", day:"numeric", year:"numeric" })}`; } } function loadPuzzleFor(date, difficulty) { currentDate = date; currentDifficulty = difficulty; const { theme, solution, puzzle } = makeDailyPuzzle(date, difficulty); currentSolution = solution; currentPuzzle = clone2D(puzzle); loadPalette(theme); renderBoard(); updateLabels(theme, date, difficulty); } function setMode(mode) { currentMode = mode; // nav buttons $$(".nav-btn").forEach(btn => { btn.classList.toggle("active", btn.dataset.mode === mode); }); // archive panel visibility const archivePanel = $("#archivePanel"); if (mode === "archive") { archivePanel.classList.remove("hidden"); } else { archivePanel.classList.add("hidden"); } if (mode === "today-normal") { loadPuzzleFor(new Date(), "normal"); } else if (mode === "today-hard") { loadPuzzleFor(new Date(), "hard"); } // archive mode loads only when user clicks "Load puzzle" } function setupNav() { $("#mode-today-normal").addEventListener("click", () => setMode("today-normal")); $("#mode-today-hard").addEventListener("click", () => setMode("today-hard")); $("#mode-archive").addEventListener("click", () => setMode("archive")); } function setupControls() { $("#clearCellBtn").addEventListener("click", clearSelectedCell); $("#checkBtn").addEventListener("click", checkSolution); $("#solveBtn").addEventListener("click", showSolution); $("#restartBtn").addEventListener("click", () => loadPuzzleFor(currentDate, currentDifficulty) ); // keyboard shortcuts: 1–9, backspace/delete document.addEventListener("keydown", e => { if (!selectedCell || selectedCell.classList.contains("given")) return; if (e.key >= "1" && e.key <= "9") { const idx = parseInt(e.key, 10) - 1; if (idx >= 0 && idx < animals.length) { const r = +selectedCell.dataset.row; const c = +selectedCell.dataset.col; place(r, c, idx, selectedCell); } } else if (e.key === "Backspace" || e.key === "Delete") { clearSelectedCell(); } }); } function setupArchive() { const dateInput = $("#archiveDate"); const diffSelect = $("#archiveDifficulty"); const loadBtn = $("#archiveLoadBtn"); // default to today const today = new Date(); dateInput.valueAsDate = today; loadBtn.addEventListener("click", () => { const picked = dateInput.valueAsDate; if (!picked) { alert("Pick a date first."); return; } const diff = diffSelect.value === "hard" ? "hard" : "normal"; loadPuzzleFor(picked, diff); }); } // ========================= // BOOT // ========================= document.addEventListener("DOMContentLoaded", () => { setupNav(); setupControls(); setupArchive(); setMode("today-normal"); });