Files
catdoku/game.js
T

730 lines
20 KiB
JavaScript

(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();
saveGameState();
}
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();
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 = `<span class="cat">${CAT_EMOJI}</span>`;
} else if (cell.hasX) {
cellEl.innerHTML = `<span class="x-mark">${X_MARK}</span>`;
}
// 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();
}
})();