(function() { 'use strict'; const COLORS = [ '#6abf69', '#4a90d9', '#e8a838', '#c06dbf', '#e06060', '#40b8c8', '#d4c830', '#8b6b4a', '#7088a0', '#d04080', '#5060b0', '#30a080' ]; const CAT_EMOJI = '\uD83D\uDC31'; const X_MARK = '\u2715'; let state = { size: 8, grid: [], solution: [], errors: 3, maxErrors: 3, catsPlaced: 0, totalCats: 0, level: 1, score: 0, hints: 3, gameOver: false, won: false, playerName: '', levelStartTime: null, levelTime: 0, sessionRecords: [] }; let pressTimer = null; let longPressTriggered = false; let gameTimer = null; const LONG_PRESS_MS = 500; // DOM refs const gridContainer = document.getElementById('grid-container'); const levelDisplay = document.getElementById('level-display'); const scoreDisplay = document.getElementById('score-display'); const catsDisplay = document.getElementById('cats-display'); const errorsDisplay = document.getElementById('errors-display'); const hintCountEl = document.getElementById('hint-count'); const modalOverlay = document.getElementById('modal-overlay'); const modalTitle = document.getElementById('modal-title'); const modalMessage = document.getElementById('modal-message'); const modalBtn = document.getElementById('modal-btn'); const gridSizeSelect = document.getElementById('grid-size'); const btnNewGame = document.getElementById('btn-new-game'); const btnHint = document.getElementById('btn-hint'); const btnEraser = document.getElementById('btn-eraser'); const timeDisplay = document.getElementById('time-display'); const playerNameDisplay = document.getElementById('player-name-display'); const nameInputOverlay = document.getElementById('name-input-overlay'); const nameInput = document.getElementById('name-input'); const nameSubmitBtn = document.getElementById('name-submit-btn'); // --- Generator --- function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } function isAdjacent(r1, c1, r2, c2) { return Math.abs(r1 - r2) <= 1 && Math.abs(c1 - c2) <= 1 && !(r1 === r2 && c1 === c2); } function canPlaceCat(grid, row, col, size, placed) { for (const p of placed) { if (p.row === row || p.col === col) return false; if (isAdjacent(p.row, p.col, row, col)) return false; } return true; } function generateSolution(size) { const placed = []; const rows = shuffle([...Array(size).keys()]); function backtrack(rowIdx) { if (rowIdx === size) return true; const row = rows[rowIdx]; const cols = shuffle([...Array(size).keys()]); for (const col of cols) { if (canPlaceCat(null, row, col, size, placed)) { placed.push({ row, col }); if (backtrack(rowIdx + 1)) return true; placed.pop(); } } return false; } // Try multiple times with different shuffles for (let attempt = 0; attempt < 100; attempt++) { placed.length = 0; for (let i = 0; i < size; i++) rows[i] = i; shuffle(rows); if (backtrack(0)) return placed; } // Fallback: simple placement placed.length = 0; for (let i = 0; i < size; i++) { placed.push({ row: i, col: (i * 2) % size }); } return placed; } function generateColors(size, solution) { const grid = Array.from({ length: size }, () => Array(size).fill(-1)); const colorCount = solution.length; const colorOrder = shuffle([...Array(colorCount).keys()]); // Assign each solution cat its color solution.forEach((cat, i) => { grid[cat.row][cat.col] = colorOrder[i]; }); // Flood fill from each cat to expand regions contiguously // Use BFS from all cats simultaneously, expanding one step at a time const queues = colorOrder.map((color, i) => { const cat = solution[i]; return [{ r: cat.row, c: cat.col }]; }); let totalFilled = colorCount; const totalCells = size * size; while (totalFilled < totalCells) { let expanded = false; // Shuffle queue order each round for randomness const order = shuffle([...Array(colorCount).keys()]); for (const i of order) { if (queues[i].length === 0) continue; const idx = Math.floor(Math.random() * queues[i].length); const { r, c } = queues[i].splice(idx, 1)[0]; const dirs = shuffle([[0,1],[0,-1],[1,0],[-1,0]]); for (const [dr, dc] of dirs) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < size && nc >= 0 && nc < size && grid[nr][nc] === -1) { grid[nr][nc] = colorOrder[i]; queues[i].push({ r: nr, c: nc }); totalFilled++; expanded = true; if (totalFilled >= totalCells) break; } } if (totalFilled >= totalCells) break; } if (!expanded) break; } // Fallback: colora eventuali celle rimaste scoperte (-1) // Assegna il colore di una cella adiacente già colorata if (totalFilled < totalCells) { for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (grid[r][c] === -1) { // Cerca una cella adiacente colorata const dirs = [[0,1],[0,-1],[1,0],[-1,0]]; for (const [dr, dc] of dirs) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < size && nc >= 0 && nc < size && grid[nr][nc] !== -1) { grid[r][c] = grid[nr][nc]; break; } } // Se ancora -1, assegna un colore casuale if (grid[r][c] === -1) { grid[r][c] = colorOrder[Math.floor(Math.random() * colorCount)]; } } } } } return grid; } function generatePuzzle(size) { const solution = generateSolution(size); const colorGrid = generateColors(size, solution); const grid = []; for (let r = 0; r < size; r++) { grid[r] = []; for (let c = 0; c < size; c++) { grid[r][c] = { color: colorGrid[r][c], hasCat: false, hasX: false, isSolution: solution.some(s => s.row === r && s.col === c) }; } } return { grid, solution }; } // --- Game Logic --- const STORAGE_KEY_STATE = 'catdoku_game_state'; function saveGameState() { const stateToSave = { size: state.size, grid: state.grid, solution: state.solution, errors: state.errors, catsPlaced: state.catsPlaced, totalCats: state.totalCats, level: state.level, score: state.score, hints: state.hints, gameOver: state.gameOver, won: state.won, playerName: state.playerName, levelStartTime: state.levelStartTime, levelTime: state.levelTime }; localStorage.setItem(STORAGE_KEY_STATE, JSON.stringify(stateToSave)); } function loadGameState() { const saved = localStorage.getItem(STORAGE_KEY_STATE); if (!saved) return null; try { return JSON.parse(saved); } catch (e) { return null; } } function clearGameState() { localStorage.removeItem(STORAGE_KEY_STATE); } function startNewGame() { // Genera sempre una nuova partita, ignorando stato salvato clearGameState(); const size = parseInt(gridSizeSelect.value); state.size = size; state.errors = state.maxErrors; state.catsPlaced = 0; state.totalCats = size; state.gameOver = false; state.won = false; state.hints = 3; state.levelStartTime = Date.now(); state.levelTime = 0; const puzzle = generatePuzzle(size); state.grid = puzzle.grid; state.solution = puzzle.solution; updateUI(); renderGrid(); hideModal(); startTimer(); // NON chiamare saveGameState() qui! Viene chiamato solo quando l'utente fa una mossa } function initGame() { // Prima prova a caricare stato salvato const savedState = loadGameState(); if (savedState && !savedState.gameOver && !savedState.won) { // Ripristina stato salvato state.size = savedState.size; state.grid = savedState.grid; state.solution = savedState.solution; state.errors = savedState.errors; state.catsPlaced = savedState.catsPlaced; state.totalCats = savedState.totalCats; state.level = savedState.level; state.score = savedState.score; state.hints = savedState.hints; state.gameOver = savedState.gameOver; state.won = savedState.won; state.playerName = savedState.playerName; state.levelStartTime = savedState.levelStartTime; state.levelTime = savedState.levelTime; // Ripristina UI if (playerNameDisplay) { playerNameDisplay.textContent = state.playerName; } updateUI(); renderGrid(); hideModal(); hideNameInput(); startTimer(); return; } // Nuova partita const size = parseInt(gridSizeSelect.value); state.size = size; state.errors = state.maxErrors; state.catsPlaced = 0; state.totalCats = size; state.gameOver = false; state.won = false; state.hints = 3; state.levelStartTime = Date.now(); state.levelTime = 0; const puzzle = generatePuzzle(size); state.grid = puzzle.grid; state.solution = puzzle.solution; updateUI(); renderGrid(); hideModal(); startTimer(); } function startTimer() { if (gameTimer) clearInterval(gameTimer); gameTimer = setInterval(() => { if (!state.gameOver && !state.won) { state.levelTime = Math.floor((Date.now() - state.levelStartTime) / 1000); updateTimeDisplay(); } }, 1000); } function stopTimer() { if (gameTimer) { clearInterval(gameTimer); gameTimer = null; } } function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } function updateTimeDisplay() { if (timeDisplay) { timeDisplay.textContent = formatTime(state.levelTime); } } function saveLevelRecord() { const record = { playerName: state.playerName, level: state.level, gridSize: state.size, time: state.levelTime, score: state.score, errors: state.maxErrors - state.errors, hintsUsed: 3 - state.hints, date: new Date().toISOString() }; state.sessionRecords.push(record); // Salva in localStorage const allRecords = JSON.parse(localStorage.getItem('catdoku_records') || '[]'); allRecords.push(record); localStorage.setItem('catdoku_records', JSON.stringify(allRecords)); } function getCatCountInRow(row) { let count = 0; for (let c = 0; c < state.size; c++) { if (state.grid[row][c].hasCat) count++; } return count; } function getCatCountInCol(col) { let count = 0; for (let r = 0; r < state.size; r++) { if (state.grid[r][col].hasCat) count++; } return count; } function getCatCountInColor(color) { let count = 0; for (let r = 0; r < state.size; r++) { for (let c = 0; c < state.size; c++) { if (state.grid[r][c].color === color && state.grid[r][c].hasCat) count++; } } return count; } function isCatAdjacent(row, col) { for (let dr = -1; dr <= 1; dr++) { for (let dc = -1; dc <= 1; dc++) { if (dr === 0 && dc === 0) continue; const nr = row + dr, nc = col + dc; if (nr >= 0 && nr < state.size && nc >= 0 && nc < state.size) { if (state.grid[nr][nc].hasCat) return true; } } } return false; } function isValidPlacement(row, col) { const cell = state.grid[row][col]; if (cell.hasX) return false; if (getCatCountInRow(row) > 0) return false; if (getCatCountInCol(col) > 0) return false; if (getCatCountInColor(cell.color) > 0) return false; if (isCatAdjacent(row, col)) return false; return true; } function placeCat(row, col) { if (state.gameOver || state.won) return; const cell = state.grid[row][col]; // If cat already there, remove it if (cell.hasCat) { cell.hasCat = false; state.catsPlaced--; renderGrid(); updateUI(); saveGameState(); return; } // Check if placement is valid if (!isValidPlacement(row, col)) { state.errors--; flashError(row, col); updateUI(); saveGameState(); if (state.errors <= 0) { state.gameOver = true; clearGameState(); showModal('Game Over!', 'Hai esaurito tutti i tentativi. Riprova!', 'Ricomincia', () => { startNewGame(); }); } return; } cell.hasCat = true; state.catsPlaced++; renderGrid(); updateUI(); saveGameState(); // Check win if (state.catsPlaced === state.totalCats) { state.won = true; stopTimer(); state.score += state.errors * 10 + state.hints * 5; saveLevelRecord(); clearGameState(); state.level++; showModal('Vittoria!', `Hai completato il livello ${state.level - 1}!\nTempo: ${formatTime(state.levelTime)}\nPunteggio: ${state.score}`, 'Livello Successivo', () => { startNewGame(); // Nuovo livello, mantiene il nome }); } } function toggleX(row, col) { if (state.gameOver || state.won) return; const cell = state.grid[row][col]; if (cell.hasCat) return; // Can't X a cat cell cell.hasX = !cell.hasX; renderGrid(); saveGameState(); } function flashError(row, col) { const cellEl = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); if (cellEl) { cellEl.classList.add('error-flash'); setTimeout(() => cellEl.classList.remove('error-flash'), 500); } } function useHint() { if (state.gameOver || state.won) return; // Se non ci sono suggerimenti, mostra popup premium if (state.hints <= 0) { showPremiumPopup(); return; } // Find a solution cat that isn't placed yet for (const sol of state.solution) { if (!state.grid[sol.row][sol.col].hasCat) { state.grid[sol.row][sol.col].hasCat = true; state.catsPlaced++; state.hints--; renderGrid(); updateUI(); saveGameState(); if (state.catsPlaced === state.totalCats) { state.won = true; stopTimer(); state.score += state.errors * 10 + state.hints * 5; saveLevelRecord(); clearGameState(); state.level++; showModal('Vittoria!', 'Hai completato il livello! Punteggio: ' + state.score, 'Livello Successivo', () => { startNewGame(); }); } return; } } } // --- Rendering --- function renderGrid() { gridContainer.innerHTML = ''; gridContainer.style.gridTemplateColumns = `repeat(${state.size}, 1fr)`; for (let r = 0; r < state.size; r++) { for (let c = 0; c < state.size; c++) { const cell = state.grid[r][c]; const cellEl = document.createElement('div'); cellEl.className = 'cell'; cellEl.dataset.row = r; cellEl.dataset.col = c; cellEl.style.backgroundColor = COLORS[cell.color % COLORS.length]; if (cell.hasCat) { cellEl.innerHTML = `${CAT_EMOJI}`; } else if (cell.hasX) { cellEl.innerHTML = `${X_MARK}`; } // Events cellEl.addEventListener('mousedown', (e) => onPointerDown(e, r, c)); cellEl.addEventListener('mouseup', (e) => onPointerUp(e, r, c)); cellEl.addEventListener('mouseleave', () => onPointerLeave()); cellEl.addEventListener('touchstart', (e) => onTouchStart(e, r, c), { passive: false }); cellEl.addEventListener('touchend', (e) => onTouchEnd(e, r, c)); cellEl.addEventListener('touchcancel', () => onPointerLeave()); cellEl.addEventListener('contextmenu', (e) => e.preventDefault()); gridContainer.appendChild(cellEl); } } } function updateUI() { levelDisplay.textContent = state.level; scoreDisplay.textContent = state.score; catsDisplay.textContent = `${state.catsPlaced}/${state.totalCats}`; hintCountEl.textContent = state.hints; // Aggiungi classe "zero" quando i suggerimenti sono esauriti hintCountEl.classList.toggle('zero', state.hints <= 0); // Update error hearts const hearts = errorsDisplay.querySelectorAll('.heart'); hearts.forEach((h, i) => { h.classList.toggle('lost', i >= state.errors); }); } // --- Input Handling --- function onPointerDown(e, row, col) { if (e.button !== 0) return; longPressTriggered = false; pressTimer = setTimeout(() => { longPressTriggered = true; placeCat(row, col); }, LONG_PRESS_MS); } function onPointerUp(e, row, col) { clearTimeout(pressTimer); if (!longPressTriggered) { toggleX(row, col); } } function onPointerLeave() { clearTimeout(pressTimer); longPressTriggered = false; } function onTouchStart(e, row, col) { e.preventDefault(); longPressTriggered = false; pressTimer = setTimeout(() => { longPressTriggered = true; placeCat(row, col); }, LONG_PRESS_MS); } function onTouchEnd(e, row, col) { clearTimeout(pressTimer); if (!longPressTriggered) { toggleX(row, col); } } // --- Modal --- function showModal(title, message, btnText, callback) { modalTitle.textContent = title; modalMessage.textContent = message; modalBtn.textContent = btnText; modalOverlay.classList.remove('hidden'); modalBtn.onclick = () => { hideModal(); if (callback) callback(); }; } function hideModal() { modalOverlay.classList.add('hidden'); } function showPremiumPopup() { const premiumOverlay = document.getElementById('premium-overlay'); const premiumBtn = document.getElementById('premium-btn'); const premiumTimer = document.getElementById('premium-timer'); premiumOverlay.classList.remove('hidden'); let countdown = 5; premiumBtn.disabled = true; premiumBtn.textContent = `Attendi ${countdown}s...`; premiumTimer.textContent = countdown; const timer = setInterval(() => { countdown--; premiumTimer.textContent = countdown; if (countdown <= 0) { clearInterval(timer); premiumBtn.disabled = false; premiumBtn.textContent = 'OK'; } else { premiumBtn.textContent = `Attendi ${countdown}s...`; } }, 1000); premiumBtn.onclick = () => { clearInterval(timer); premiumOverlay.classList.add('hidden'); state.hints++; updateUI(); }; } // --- Event Listeners --- btnNewGame.addEventListener('click', () => { state.level = 1; state.score = 0; state.hints = 3; state.sessionRecords = []; clearGameState(); state.playerName = ''; showNameInput(); }); btnHint.addEventListener('click', useHint); btnEraser.addEventListener('click', () => { // Clear all X marks (not cats) for (let r = 0; r < state.size; r++) { for (let c = 0; c < state.size; c++) { state.grid[r][c].hasX = false; } } renderGrid(); }); // Name input handling function showNameInput() { nameInputOverlay.classList.remove('hidden'); nameInput.value = state.playerName || ''; nameInput.focus(); } function hideNameInput() { nameInputOverlay.classList.add('hidden'); } nameSubmitBtn.addEventListener('click', () => { const name = nameInput.value.trim(); if (name) { state.playerName = name; if (playerNameDisplay) { playerNameDisplay.textContent = name; } hideNameInput(); startNewGame(); // Nuova partita, non initGame che carica stato salvato } }); nameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { nameSubmitBtn.click(); } }); // Init - controlla se c'è una partita salvata const savedState = loadGameState(); if (savedState && savedState.playerName && savedState.grid && savedState.grid.length > 0 && !savedState.gameOver && !savedState.won) { // Ripristina partita in corso, salta input nome initGame(); } else { // Prima partita o stato invalido clearGameState(); showNameInput(); } })();