commit 642f568ac6db7996eeb477c1b865b4adb714c54c Author: truetype Date: Mon Jun 15 18:40:07 2026 +0200 Initial commit: CatDoku puzzle game with leaderboard diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b04e5ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production +/dist +/build + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory +/coverage + +# Nyc for coverage +/.nyc_output + +# Environment +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db +desktop.ini + +# Browser +*.local + +# Testing +/test-results +/junit.xml + +# Misc +.cache +.temp +tmp/ +temp/ + +# Documentation generated +docs/api-generated/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..86c835a --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +# CatDoku - Puzzle dei Gatti + +![CatDoku](https://img.shields.io/badge/version-1.0.0-blue) +![License](https://img.shields.io/badge/license-MIT-green) +![Platform](https://img.shields.io/badge/platform-Web-orange) + +## 📖 Descrizione + +**CatDoku** è un puzzle game ispirato al classico Sudoku, ma con una meccanica unica basata sul posizionamento di gatti su una griglia colorata. Il gioco combina logica, strategia e divertimento in un'esperienza web moderna e responsive. + +### 🎯 Obiettivo del Gioco + +Posizionare un gatto in ogni riga, colonna e regione colorata della griglia, rispettando il vincolo che nessun gatto può essere adiacente (in qualsiasi direzione, incluse le diagonali) ad un altro gatto. + +## ✨ Caratteristiche Principali + +### 🎮 Gameplay +- **Griglie dinamiche**: Supporto per griglie da 6×6 fino a 12×12 +- **Generazione procedurale**: Ogni partita genera un puzzle unico e sempre risolvibile +- **Regioni colorate contigue**: I colori formano aree connesse per una migliore esperienza visiva +- **Sistema di errori**: 3 tentativi massimi per livello con feedback visivo immediato +- **Suggerimenti**: 3 hint gratuiti per livello con sistema premium per suggerimenti aggiuntivi + +### Interfaccia +- **Design moderno**: UI pulita e intuitiva con animazioni fluide +- **Responsive**: Giocabile su desktop, tablet e mobile +- **Interazione touch**: Supporto completo per dispositivi touch + - Click singolo: posiziona/rimuove X + - Pressione prolungata (0.5s): posiziona/rimuove gatto +- **Feedback visivo**: Animazioni per errori, posizionamenti e vittorie + +### Sistema Player +- **Alias personalizzati**: Ogni giocatore può inserire il proprio nome/alias +- **Tracking tempo**: Timer per ogni livello con formato mm:ss +- **Salvataggio locale**: Progressi salvati in localStorage +- **Classifica**: Leaderboard completa con filtri e statistiche + +### 🏆 Leaderboard +- **Statistiche globali**: Partite giocate, giocatori unici, miglior tempo, miglior punteggio +- **Filtri avanzati**: Per giocatore, livello e dimensione griglia +- **Medaglie**: Prime 3 posizioni con medaglie d'oro, argento e bronzo +- **Cronologia**: Data e ora di ogni partita completata + +### 💎 Sistema Premium +- **Popup premium**: Attivato quando si esauriscono i suggerimenti +- **Countdown**: 5 secondi di attesa prima di poter ottenere un hint aggiuntivo +- **Design accattivante**: UI dorata con animazioni + +## 🚀 Installazione e Avvio + +### Requisiti +- Browser moderno con supporto JavaScript ES6+ +- Nessun server richiesto (funziona con file:// protocol) + +### Avvio Rapido + +1. **Clona il repository**: + ```bash + git clone http://192.168.0.183:8085/truetype/catdoku.git + cd catdoku + ``` + +2. **Apri il gioco**: + ```bash + # Metodo 1: Apri direttamente nel browser + open index.html + + # Metodo 2: Usa un server HTTP locale (opzionale) + python3 -m http.server 8000 + # Poi visita http://localhost:8000 + ``` + +3. **Inizia a giocare**: + - Inserisci il tuo nome/alias + - Seleziona la dimensione della griglia (6×6 fino a 12×12) + - Clicca "Nuova Partita" + +## 🎮 Come Giocare + +### Controlli + +| Azione | Mouse | Touch | +|--------|-------|-------| +| Posiziona/Rimuovi X | Click singolo | Tap singolo | +| Posiziona/Rimuovi Gatto | Pressione 0.5s | Pressione 0.5s | +| Cancella tutte le X | Pulsante gomma | Pulsante gomma | +| Suggerimento | Pulsante lampadina | Pulsante lampadina | + +### Regole del Gioco + +1. **Un gatto per riga**: Ogni riga deve contenere esattamente un gatto +2. **Un gatto per colonna**: Ogni colonna deve contenere esattamente un gatto +3. **Un gatto per colore**: Ogni regione colorata deve contenere esattamente un gatto +4. **Nessuna adiacenza**: I gatti non possono essere adiacenti (incluse diagonali) + +### Sistema di Punteggio + +Il punteggio viene calcolato in base a: +- **Errori rimanenti**: 10 punti per ogni errore non commesso +- **Suggerimenti rimanenti**: 5 punti per ogni hint non utilizzato +- **Tempo**: Non influisce sul punteggio ma viene tracciato per la leaderboard + +## 🛠️ Sviluppo + +### Struttura del Progetto + +``` +catdoku/ +├── index.html # Pagina principale del gioco +├── style.css # Stili e layout responsive +── game.js # Logica di gioco e generatore di puzzle +├── leaderboard.html # Pagina classifica +├── README.md # Documentazione principale +└── docs/ # Documentazione tecnica + ├── architecture.md # Architettura del sistema + ├── game-rules.md # Regole del gioco dettagliate + ├── generator.md # Algoritmo di generazione puzzle + └── api.md # API e struttura dati +``` + +### Tecnologie Utilizzate + +- **HTML5**: Struttura semantica +- **CSS3**: Stili moderni con Grid, Flexbox, animazioni +- **JavaScript ES6+**: Logica di gioco, generatore procedurale +- **localStorage API**: Persistenza dati locale + +### Architettura + +Il progetto segue un'architettura **MVC semplificata**: + +- **Model**: Stato del gioco (griglia, soluzione, punteggio, timer) +- **View**: Rendering DOM e gestione eventi UI +- **Controller**: Logica di gioco, validazione, generazione puzzle + +### Generatore di Puzzle + +Il generatore utilizza un algoritmo in 3 fasi: + +1. **Backtracking randomizzato**: Posiziona N gatti validi sulla griglia +2. **Flood fill simultaneo**: Espande regioni colorate contigue da ogni gatto +3. **Validazione**: Garantisce puzzle sempre risolvibile con soluzione unica + +## 📊 Roadmap Futura + +- [ ] Livelli predefiniti con difficoltà crescente +- [ ] Sistema di achievement/badge +- [ ] Modalità sfida a tempo +- [ ] Condivisione punteggi sui social +- [ ] Tema scuro +- [ ] Suoni ed effetti audio +- [ ] Multiplayer locale (2 giocatori) +- [ ] Esportazione/importazione record + +## 🤝 Contribuire + +Le contribuzioni sono benvenute! Per contribuire: + +1. Fork il progetto +2. Crea un branch per la tua feature (`git checkout -b feature/AmazingFeature`) +3. Commit le tue modifiche (`git commit -m 'Add some AmazingFeature'`) +4. Push sul branch (`git push origin feature/AmazingFeature`) +5. Apri una Pull Request + +## License + +Questo progetto è distribuito sotto licenza MIT. Vedi il file `LICENSE` per maggiori informazioni. + +## 👤 Autore + +**truetype** + +- Server Gitea: http://192.168.0.183:8085 +- Repository: http://192.168.0.183:8085/truetype/catdoku + +## 🙏 Ringraziamenti + +- Ispirato al gioco "Clash of Critters" +- Icone e emoji standard Unicode +- Font di sistema per massima compatibilità + +--- + +**Buon divertimento con CatDoku! 🐱** diff --git a/WhatsApp Image 2026-06-15 at 11.41.31.jpeg b/WhatsApp Image 2026-06-15 at 11.41.31.jpeg new file mode 100644 index 0000000..f39ed02 Binary files /dev/null and b/WhatsApp Image 2026-06-15 at 11.41.31.jpeg differ diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..c30b4a6 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,447 @@ +# API e Struttura Dati + +## Panoramica + +Questo documento descrive la struttura dei dati, le interfacce e i moduli del gioco CatDoku. Essendo un'applicazione client-side, non ci sono API REST, ma il documento descrive le interfacce interne e la struttura di storage. + +## Struttura Dati Principali + +### Game State + +Oggetto principale che contiene tutto lo stato del gioco: + +```typescript +interface GameState { + size: number; // Dimensione griglia (6-12) + grid: Cell[][]; // Griglia 2D delle celle + solution: Position[]; // Array delle posizioni soluzione + errors: number; // Errori rimanenti (0-3) + maxErrors: number; // Errori massimi (3) + catsPlaced: number; // Numero gatti piazzati + totalCats: number; // Numero totale gatti (= size) + level: number; // Livello corrente (1+) + score: number; // Punteggio totale accumulato + hints: number; // Suggerimenti rimanenti (0-3) + gameOver: boolean; // Flag game over + won: boolean; // Flag vittoria livello + playerName: string; // Nome/alias del giocatore + levelStartTime: number; // Timestamp inizio livello (ms) + levelTime: number; // Tempo trascorso (secondi) + sessionRecords: LevelRecord[]; // Record della sessione corrente +} +``` + +### Cell + +Rappresenta una singola cella della griglia: + +```typescript +interface Cell { + color: number; // Indice colore (0-11) + hasCat: boolean; // Contiene un gatto? + hasX: boolean; // Contiene una X (marcatura giocatore)? + isSolution: boolean; // È parte della soluzione? +} +``` + +### Position + +Coordinate di una cella: + +```typescript +interface Position { + row: number; // Riga (0-indexed) + col: number; // Colonna (0-indexed) +} +``` + +### LevelRecord + +Record di un livello completato: + +```typescript +interface LevelRecord { + playerName: string; // Nome giocatore + level: number; // Livello completato + gridSize: number; // Dimensione griglia usata + time: number; // Tempo impiegato (secondi) + score: number; // Punteggio ottenuto + errors: number; // Errori commessi + hintsUsed: number; // Suggerimenti utilizzati + date: string; // Data ISO 8601 +} +``` + +## Interfacce dei Moduli + +### Generator Module + +```typescript +interface GeneratorModule { + generatePuzzle(size: number): Puzzle; + generateSolution(size: number): Position[]; + generateColors(size: number, solution: Position[]): number[][]; +} + +interface Puzzle { + grid: Cell[][]; + solution: Position[]; +} +``` + +#### generatePuzzle(size) +- **Input**: `size` - Dimensione griglia (6-12) +- **Output**: Oggetto `Puzzle` con griglia e soluzione +- **Effetti collaterali**: Nessuno +- **Eccezioni**: Nessuna (ha fallback deterministico) + +#### generateSolution(size) +- **Input**: `size` - Dimensione griglia +- **Output**: Array di `Position` (lunghezza = size) +- **Garantisce**: Soluzione valida (rispetta tutte le regole) +- **Eccezioni**: Nessuna + +#### generateColors(size, solution) +- **Input**: + - `size` - Dimensione griglia + - `solution` - Array di posizioni soluzione +- **Output**: Griglia 2D di indici colore +- **Garantisce**: Regioni contigue e bilanciate +- **Eccezioni**: Nessuna + +### Validation Module + +```typescript +interface ValidationModule { + isValidPlacement(row: number, col: number): boolean; + getCatCountInRow(row: number): number; + getCatCountInCol(col: number): number; + getCatCountInColor(color: number): number; + isCatAdjacent(row: number, col: number): boolean; +} +``` + +#### isValidPlacement(row, col) +- **Input**: Coordinate della cella +- **Output**: `true` se il posizionamento è valido +- **Controlla**: + 1. Cella non ha X + 2. Nessun gatto nella riga + 3. Nessun gatto nella colonna + 4. Nessun gatto nello stesso colore + 5. Nessun gatto adiacente (8 direzioni) + +#### getCatCountInRow(row) +- **Input**: Numero riga +- **Output**: Numero di gatti nella riga (0-1) + +#### getCatCountInCol(col) +- **Input**: Numero colonna +- **Output**: Numero di gatti nella colonna (0-1) + +#### getCatCountInColor(color) +- **Input**: Indice colore +- **Output**: Numero di gatti nel colore (0-1) + +#### isCatAdjacent(row, col) +- **Input**: Coordinate della cella +- **Output**: `true` se c'è un gatto adiacente (8 direzioni) + +### Game Logic Module + +```typescript +interface GameLogicModule { + initGame(): void; + placeCat(row: number, col: number): void; + toggleX(row: number, col: number): void; + useHint(): void; + startTimer(): void; + stopTimer(): void; + saveLevelRecord(): void; +} +``` + +#### initGame() +- **Input**: Nessuno (usa `state.size` e `state.playerName`) +- **Output**: Nessuno +- **Effetti**: + - Resetta stato livello + - Genera nuovo puzzle + - Avvia timer + - Renderizza griglia + +#### placeCat(row, col) +- **Input**: Coordinate della cella +- **Output**: Nessuno +- **Effetti**: + - Se gatto già presente: lo rimuove + - Se posizionamento valido: piazza gatto + - Se posizionamento invalido: decrementa errori, flash errore + - Se errori = 0: game over + - Se tutti i gatti piazzati: vittoria + +#### toggleX(row, col) +- **Input**: Coordinate della cella +- **Output**: Nessuno +- **Effetti**: + - Se cella ha gatto: nessuna azione + - Altrimenti: toggle stato X + +#### useHint() +- **Input**: Nessuno +- **Output**: Nessuno +- **Effetti**: + - Se hints = 0: mostra popup premium + - Altrimenti: piazza gatto soluzione, decrementa hints + +#### startTimer() +- **Input**: Nessuno +- **Output**: Nessuno +- **Effetti**: Avvia setInterval per aggiornamento tempo + +#### stopTimer() +- **Input**: Nessuno +- **Output**: Nessuno +- **Effetti**: Ferma timer + +#### saveLevelRecord() +- **Input**: Nessuno (usa stato corrente) +- **Output**: Nessuno +- **Effetti**: + - Crea record livello + - Salva in `state.sessionRecords` + - Salva in `localStorage` + +### UI Module + +```typescript +interface UIModule { + renderGrid(): void; + updateUI(): void; + updateTimeDisplay(): void; + showModal(title: string, message: string, btnText: string, callback: Function): void; + hideModal(): void; + showPremiumPopup(): void; + showNameInput(): void; + hideNameInput(): void; +} +``` + +#### renderGrid() +- **Input**: Nessuno (usa `state.grid`) +- **Output**: Nessuno +- **Effetti**: Ricostruisce DOM griglia con event listeners + +#### updateUI() +- **Input**: Nessuno (usa `state`) +- **Output**: Nessuno +- **Effetti**: Aggiorna tutti i contatori e indicatori UI + +#### showModal(title, message, btnText, callback) +- **Input**: Testo modal e callback +- **Output**: Nessuno +- **Effetti**: Mostra modal overlay + +#### hideModal() +- **Input**: Nessuno +- **Output**: Nessuno +- **Effetti**: Nasconde modal overlay + +#### showPremiumPopup() +- **Input**: Nessuno +- **Output**: Nessuno +- **Effetti**: Mostra popup premium con countdown 5s + +#### showNameInput() +- **Input**: Nessuno +- **Output**: Nessuno +- **Effetti**: Mostra modal input nome + +#### hideNameInput() +- **Input**: Nessuno +- **Output**: Nessuno +- **Effetti**: Nasconde modal input nome + +### Storage Module + +```typescript +interface StorageModule { + loadRecords(): LevelRecord[]; + saveRecords(records: LevelRecord[]): void; +} +``` + +#### loadRecords() +- **Input**: Nessuno +- **Output**: Array di `LevelRecord` +- **Source**: `localStorage` key `'catdoku_records'` + +#### saveRecords(records) +- **Input**: Array di `LevelRecord` +- **Output**: Nessuno +- **Destination**: `localStorage` key `'catdoku_records'` + +## Costanti + +### COLORS + +Array di colori per le regioni: + +```javascript +const COLORS = [ + '#6abf69', // Verde + '#4a90d9', // Blu + '#e8a838', // Arancione + '#c06dbf', // Viola + '#e06060', // Rosso + '#40b8c8', // Ciano + '#d4c830', // Giallo + '#8b6b4a', // Marrone + '#7088a0', // Grigio-blu + '#d04080', // Rosa + '#5060b0', // Blu scuro + '#30a080' // Verde acqua +]; +``` + +### LONG_PRESS_MS + +Durata pressione per attivare gatto: + +```javascript +const LONG_PRESS_MS = 500; // millisecondi +``` + +### STORAGE_KEY + +Key per localStorage: + +```javascript +const STORAGE_KEY = 'catdoku_records'; +``` + +## Eventi UI + +### Grid Cell Events + +| Evento | Azione | Condizione | +|--------|--------|------------| +| `mousedown` | Avvia timer long press | Button 0 (sinistro) | +| `mouseup` | Toggle X o place cat | Se long press non attivato → X | +| `mouseleave` | Cancella timer | Sempre | +| `touchstart` | Avvia timer long press | Prevent default | +| `touchend` | Toggle X o place cat | Se long press non attivato → X | +| `touchcancel` | Cancella timer | Sempre | +| `contextmenu` | Prevent default | Sempre (no menu contestuale) | + +### Button Events + +| Button | Evento | Azione | +|--------|--------|--------| +| `btn-new-game` | click | Reset partita, mostra input nome | +| `btn-hint` | click | Usa suggerimento o mostra premium | +| `btn-eraser` | click | Cancella tutte le X | +| `btn-back` | click | Naviga a leaderboard.html | +| `name-submit-btn` | click | Conferma nome, inizia gioco | +| `premium-btn` | click | Chiudi popup, aggiungi hint | +| `modal-btn` | click | Chiudi modal, esegui callback | + +## LocalStorage Schema + +### Key: `catdoku_records` + +**Type**: JSON string + +**Structure**: Array di `LevelRecord` + +**Example**: +```json +[ + { + "playerName": "Mario", + "level": 1, + "gridSize": 8, + "time": 125, + "score": 35, + "errors": 1, + "hintsUsed": 2, + "date": "2026-06-15T18:30:00.000Z" + }, + { + "playerName": "Mario", + "level": 2, + "gridSize": 8, + "time": 98, + "score": 40, + "errors": 0, + "hintsUsed": 1, + "date": "2026-06-15T18:35:00.000Z" + } +] +``` + +**Limiti**: +- Dimensione massima: ~5MB (limite browser) +- Persistenza: Fino a cancellazione manuale o cache clear +- Scope: Per-browser, per-origin + +## Error Handling + +### Errori di Gioco + +Gestiti tramite decremento `state.errors` e flash visivo. + +### Errori di Sistema + +- **Generazione fallita**: Fallback a posizionamento deterministico +- **localStorage pieno**: Nessun handling specifico (raro per questo uso) +- **Input invalido**: Trim e maxlength su nome giocatore + +## Security Considerations + +### Input Sanitization + +- **Nome giocatore**: `trim()` + `maxlength="20"` nell'HTML +- **Nessun eval()**: Tutto il codice è statico +- **No dynamic code**: Nessun `Function()` o `setTimeout()` con stringhe + +### Data Validation + +- **Dimensione griglia**: Validata da ` + + + + + + + + + + + + + + + + + + + + + diff --git a/leaderboard.html b/leaderboard.html new file mode 100644 index 0000000..d136ed0 --- /dev/null +++ b/leaderboard.html @@ -0,0 +1,404 @@ + + + + + + CatDoku - Classifica + + + + +
+
+ ← Torna al Gioco + +
+

🏆 Classifica CatDoku

+

I migliori tempi e punteggi di tutti i giocatori

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+ + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..f2868bc --- /dev/null +++ b/style.css @@ -0,0 +1,442 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: #f5f0eb; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 10px; + user-select: none; + -webkit-user-select: none; +} + +#app { + max-width: 600px; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 8px 0; +} + +.header-center { + display: flex; + gap: 30px; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-label { + font-size: 14px; + color: #8b7355; + font-weight: 500; +} + +.stat-value { + font-size: 28px; + font-weight: 700; + color: #5d4e37; +} + +.header-right { + display: flex; + align-items: center; + gap: 10px; +} + +.player-name { + font-size: 14px; + color: #8b7355; + font-weight: 600; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.icon-btn { + width: 40px; + height: 40px; + border-radius: 50%; + border: 2px solid #d4c5b0; + background: #fff; + font-size: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: #8b7355; + transition: background 0.2s; +} + +.icon-btn:hover { + background: #f0e8dc; +} + +.info-bar { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 8px 16px; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); +} + +.progress { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: 600; + color: #5d4e37; +} + +.cat-icon { + font-size: 24px; +} + +.errors { + display: flex; + gap: 4px; + font-size: 22px; +} + +.heart { + transition: opacity 0.3s; +} + +.heart.lost { + opacity: 0.2; +} + +.rules-bar { + display: flex; + gap: 8px; + width: 100%; + flex-wrap: wrap; + justify-content: center; +} + +.rule { + background: #fff; + padding: 6px 14px; + border-radius: 20px; + font-size: 13px; + color: #8b7355; + box-shadow: 0 1px 4px rgba(0,0,0,0.05); + white-space: nowrap; +} + +.grid-container { + display: grid; + gap: 2px; + background: #d4c5b0; + padding: 3px; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0,0,0,0.1); + width: 100%; + max-width: 500px; + aspect-ratio: 1; +} + +.cell { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + cursor: pointer; + transition: transform 0.1s, filter 0.1s; + position: relative; + border-radius: 3px; +} + +.cell:hover { + filter: brightness(1.1); +} + +.cell:active { + transform: scale(0.95); +} + +.cell .cat { + font-size: 22px; + pointer-events: none; + animation: catPlace 0.3s ease; +} + +.cell .x-mark { + font-size: 18px; + color: rgba(255,255,255,0.85); + font-weight: 700; + pointer-events: none; + text-shadow: 0 1px 2px rgba(0,0,0,0.3); +} + +.cell.error-flash { + animation: errorFlash 0.5s ease; +} + +@keyframes catPlace { + 0% { transform: scale(0); opacity: 0; } + 50% { transform: scale(1.3); } + 100% { transform: scale(1); opacity: 1; } +} + +@keyframes errorFlash { + 0%, 100% { filter: brightness(1); } + 25% { filter: brightness(2) hue-rotate(320deg); } + 50% { filter: brightness(0.8); } + 75% { filter: brightness(1.5) hue-rotate(320deg); } +} + +.controls { + display: flex; + gap: 20px; + align-items: center; +} + +.tool-btn { + width: 50px; + height: 50px; + border-radius: 50%; + border: none; + background: #fff; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + transition: transform 0.2s, box-shadow 0.2s; + position: relative; +} + +.tool-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +.tool-btn:active { + transform: translateY(0); +} + +.hint-count { + position: absolute; + top: -4px; + right: -4px; + background: #e74c3c; + color: #fff; + font-size: 11px; + font-weight: 700; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.hint-count.zero { + background: #d4af37; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +.size-selector { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + justify-content: center; + padding: 10px; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); +} + +.size-selector label { + font-size: 14px; + color: #8b7355; + font-weight: 500; +} + +.size-selector select { + padding: 6px 10px; + border-radius: 8px; + border: 2px solid #d4c5b0; + font-size: 14px; + color: #5d4e37; + background: #fff; + cursor: pointer; +} + +.size-selector button { + padding: 8px 18px; + border-radius: 8px; + border: none; + background: #5d4e37; + color: #fff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.size-selector button:hover { + background: #7a6750; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.modal-overlay.hidden { + display: none; +} + +.modal { + background: #fff; + border-radius: 16px; + padding: 30px 40px; + text-align: center; + max-width: 350px; + width: 90%; + box-shadow: 0 8px 32px rgba(0,0,0,0.2); +} + +.modal h2 { + font-size: 24px; + color: #5d4e37; + margin-bottom: 12px; +} + +.modal p { + font-size: 16px; + color: #8b7355; + margin-bottom: 20px; +} + +.modal-btn { + padding: 10px 30px; + border-radius: 10px; + border: none; + background: #5d4e37; + color: #fff; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.modal-btn:hover { + background: #7a6750; +} + +/* Premium Modal Styles */ +.premium-modal { + background: linear-gradient(135deg, #fff 0%, #f8f4ff 100%); + border: 3px solid #d4af37; + box-shadow: 0 8px 32px rgba(212, 175, 55, 0.3); +} + +.premium-icon { + font-size: 60px; + margin-bottom: 10px; +} + +.premium-text { + font-size: 14px; + color: #666; + font-style: italic; + margin: 10px 0; +} + +.premium-timer { + font-size: 16px; + color: #5d4e37; + margin: 15px 0; + font-weight: 600; +} + +.timer-value { + font-size: 24px; + color: #e74c3c; + font-weight: 700; +} + +.premium-btn { + background: #d4af37; + transition: background 0.3s, opacity 0.3s; +} + +.premium-btn:hover:not(:disabled) { + background: #b8941f; +} + +.premium-btn:disabled { + background: #ccc; + cursor: not-allowed; + opacity: 0.6; +} + +/* Name Input Modal Styles */ +.name-modal { + background: linear-gradient(135deg, #fff 0%, #f0f8ff 100%); + border: 3px solid #4a90d9; + box-shadow: 0 8px 32px rgba(74, 144, 217, 0.3); +} + +.name-input { + width: 100%; + padding: 12px 16px; + border: 2px solid #d4c5b0; + border-radius: 10px; + font-size: 18px; + text-align: center; + margin: 15px 0; + outline: none; + transition: border-color 0.2s; +} + +.name-input:focus { + border-color: #4a90d9; +} + +.name-modal h2 { + color: #4a90d9; + margin-bottom: 10px; +}