// Full-Featured Tablet Crossword App class TabletCrosswordApp { constructor() { this.currentLevel = 1 this.coins = 100 this.hints = 3 this.currentPuzzle = null this.userAnswers = {} this.selectedCell = null this.selectedWord = null this.gridElement = document.getElementById('crosswordGrid') this.cells = [] this.init() } init() { this.loadGameState() this.loadPuzzle(this.currentLevel) this.setupEventListeners() this.updateUI() this.enableFullscreen() } enableFullscreen() { // Request fullscreen on first interaction document.addEventListener('click', () => { if (document.documentElement.requestFullscreen && !document.fullscreenElement) { document.documentElement.requestFullscreen().catch(() => {}) } }, { once: true }) } setupEventListeners() { // Menu document.getElementById('menuBtn').addEventListener('click', () => { document.getElementById('sideMenu').classList.add('active') }) document.getElementById('closeMenu').addEventListener('click', () => { document.getElementById('sideMenu').classList.remove('active') }) // On-screen keyboard document.querySelectorAll('.key-btn').forEach(btn => { if (!btn.id) { btn.addEventListener('click', () => { const letter = btn.textContent.trim() this.handleLetterInput(letter) }) } }) document.getElementById('delKey').addEventListener('click', () => this.handleDelete()) document.getElementById('hintKey').addEventListener('click', () => this.useHintForCurrentWord()) // Actions document.getElementById('checkBtn').addEventListener('click', () => this.checkAllAnswers()) document.getElementById('clearBtn').addEventListener('click', () => this.clearGrid()) // Menu items document.getElementById('newGame').addEventListener('click', () => this.newGame()) document.getElementById('dailyPuzzle').addEventListener('click', () => { this.showToast('Dagelijkse puzzel komt binnenkort!') document.getElementById('sideMenu').classList.remove('active') }) document.getElementById('levelSelect').addEventListener('click', () => { this.showToast('Niveau selectie komt binnenkort!') document.getElementById('sideMenu').classList.remove('active') }) document.getElementById('nextBtn').addEventListener('click', () => this.nextPuzzle()) // Prevent accidental zoom/scroll document.addEventListener('gesturestart', e => e.preventDefault()) document.addEventListener('gesturechange', e => e.preventDefault()) } loadPuzzle(level) { if (typeof DUTCH_PUZZLES !== 'undefined') { this.currentPuzzle = DUTCH_PUZZLES[`level${level}`] || DUTCH_PUZZLES.level1 } this.renderGrid() this.updateProgress() this.updateUI() } renderGrid() { if (!this.currentPuzzle) return const grid = this.currentPuzzle.grid const words = this.currentPuzzle.words this.gridElement.innerHTML = '' this.cells = [] const rows = grid.length const cols = Math.max(...grid.map(row => row.length)) + 1 const cellSize = this.calculateCellSize(rows, cols) this.gridElement.style.gridTemplateColumns = `repeat(${cols}, ${cellSize}px)` this.gridElement.style.gridTemplateRows = `repeat(${rows}, ${cellSize}px)` // Create cells for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { if (col === 0) { const clueData = this.getClueForRow(row, words) this.gridElement.appendChild(this.createClueCell(clueData, cellSize)) } else { const gridCol = col - 1 const cellValue = grid[row]?.[gridCol] || '#' const cell = this.createCell(cellValue, row, gridCol, words, cellSize) this.gridElement.appendChild(cell) if (cell.classList.contains('letter-cell')) { this.cells.push({ element: cell, row, col: gridCol }) } } } } } calculateCellSize(rows, cols) { const headerHeight = 66 const progressHeight = 40 const keyboardHeight = 230 const padding = 30 const availableHeight = window.innerHeight - headerHeight - progressHeight - keyboardHeight - padding const availableWidth = window.innerWidth - padding const maxFromHeight = Math.floor(availableHeight / rows) const maxFromWidth = Math.floor(availableWidth / cols) let cellSize = Math.min(maxFromHeight, maxFromWidth) cellSize = Math.max(50, Math.min(75, cellSize)) return cellSize } getClueForRow(row, words) { return words.find(w => w.direction === 'horizontal' && w.startRow === row) } createClueCell(clueData, cellSize) { const cell = document.createElement('div') cell.className = 'grid-cell clue-cell' cell.style.width = `${cellSize}px` cell.style.height = `${cellSize}px` if (clueData) { const text = document.createElement('div') text.className = 'clue-text' text.textContent = clueData.clue const arrow = document.createElement('div') arrow.className = 'arrow' arrow.textContent = '→' cell.appendChild(text) cell.appendChild(arrow) } else { cell.classList.add('blocked-cell') } return cell } createCell(value, row, col, words, cellSize) { const cell = document.createElement('div') cell.className = 'grid-cell' cell.style.width = `${cellSize}px` cell.style.height = `${cellSize}px` cell.dataset.row = row cell.dataset.col = col if (value === '#') { const clueData = this.getClueAtPosition(row, col, words) if (clueData) { cell.classList.add('clue-cell') const text = document.createElement('div') text.className = 'clue-text' text.textContent = clueData.clue const arrow = document.createElement('div') arrow.className = 'arrow' arrow.textContent = '→' cell.appendChild(text) cell.appendChild(arrow) } else { cell.classList.add('blocked-cell') } } else { cell.classList.add('letter-cell') const key = `${row}-${col}` cell.textContent = this.userAnswers[key] || '' // Click to select and highlight word cell.addEventListener('click', () => { this.selectCell(cell, row, col) this.highlightWord(row, col) }) } return cell } getClueAtPosition(row, col, words) { for (const word of words) { if (word.direction === 'horizontal' && word.startRow === row && word.startCol - 1 === col) { return word } } return null } selectCell(cell, row, col) { this.cells.forEach(c => c.element.classList.remove('active')) cell.classList.add('active') this.selectedCell = { element: cell, row, col } } highlightWord(row, col) { // Clear previous highlights this.cells.forEach(c => c.element.classList.remove('highlighted')) // Find word containing this cell const word = this.findWordContainingCell(row, col) if (!word) return this.selectedWord = word // Highlight all cells in this word if (word.direction === 'horizontal') { for (let c = word.startCol; c < word.startCol + word.answer.length; c++) { const cellData = this.cells.find(cell => cell.row === word.startRow && cell.col === c) if (cellData) { cellData.element.classList.add('highlighted') } } } else { for (let r = word.startRow; r < word.startRow + word.answer.length; r++) { const cellData = this.cells.find(cell => cell.row === r && cell.col === word.startCol) if (cellData) { cellData.element.classList.add('highlighted') } } } } findWordContainingCell(row, col) { for (const word of this.currentPuzzle.words) { if (word.direction === 'horizontal') { if (word.startRow === row && col >= word.startCol && col < word.startCol + word.answer.length) { return word } } else if (word.direction === 'vertical') { if (word.startCol === col && row >= word.startRow && row < word.startRow + word.answer.length) { return word } } } return null } handleLetterInput(letter) { if (!this.selectedCell) { // Auto-select first empty cell const firstEmpty = this.cells.find(c => !c.element.textContent) if (firstEmpty) { this.selectCell(firstEmpty.element, firstEmpty.row, firstEmpty.col) this.highlightWord(firstEmpty.row, firstEmpty.col) } else { return } } const { element, row, col } = this.selectedCell element.textContent = letter const key = `${row}-${col}` this.userAnswers[key] = letter this.saveGameState() this.updateProgress() // Check if word is complete this.checkWordComplete() // Move to next cell in word this.moveToNextInWord() } handleDelete() { if (!this.selectedCell) return const { element, row, col } = this.selectedCell if (element.textContent) { // Delete current cell element.textContent = '' const key = `${row}-${col}` delete this.userAnswers[key] element.classList.remove('correct', 'incorrect') } else { // Move to previous cell and delete this.moveToPreviousInWord() if (this.selectedCell) { const { element: prevElement, row: prevRow, col: prevCol } = this.selectedCell prevElement.textContent = '' const key = `${prevRow}-${prevCol}` delete this.userAnswers[key] prevElement.classList.remove('correct', 'incorrect') } } this.saveGameState() this.updateProgress() } moveToNextInWord() { if (!this.selectedCell || !this.selectedWord) return const { row, col } = this.selectedCell const word = this.selectedWord let nextRow = row let nextCol = col if (word.direction === 'horizontal') { nextCol++ if (nextCol >= word.startCol + word.answer.length) return } else { nextRow++ if (nextRow >= word.startRow + word.answer.length) return } const nextCell = this.cells.find(c => c.row === nextRow && c.col === nextCol) if (nextCell) { this.selectCell(nextCell.element, nextCell.row, nextCell.col) } } moveToPreviousInWord() { if (!this.selectedCell || !this.selectedWord) return const { row, col } = this.selectedCell const word = this.selectedWord let prevRow = row let prevCol = col if (word.direction === 'horizontal') { prevCol-- if (prevCol < word.startCol) return } else { prevRow-- if (prevRow < word.startRow) return } const prevCell = this.cells.find(c => c.row === prevRow && c.col === prevCol) if (prevCell) { this.selectCell(prevCell.element, prevCell.row, prevCell.col) } } checkWordComplete() { if (!this.selectedWord) return const word = this.selectedWord let userWord = '' let allFilled = true if (word.direction === 'horizontal') { for (let c = word.startCol; c < word.startCol + word.answer.length; c++) { const key = `${word.startRow}-${c}` const letter = this.userAnswers[key] if (!letter) { allFilled = false break } userWord += letter } } else { for (let r = word.startRow; r < word.startRow + word.answer.length; r++) { const key = `${r}-${word.startCol}` const letter = this.userAnswers[key] if (!letter) { allFilled = false break } userWord += letter } } if (allFilled) { // Auto-validate word if (userWord === word.answer) { this.animateWordSuccess(word) } else { this.animateWordError(word) } } } animateWordSuccess(word) { const cells = [] if (word.direction === 'horizontal') { for (let c = word.startCol; c < word.startCol + word.answer.length; c++) { const cellData = this.cells.find(cell => cell.row === word.startRow && cell.col === c) if (cellData) cells.push(cellData.element) } } else { for (let r = word.startRow; r < word.startRow + word.answer.length; r++) { const cellData = this.cells.find(cell => cell.row === r && cell.col === word.startCol) if (cellData) cells.push(cellData.element) } } cells.forEach((cell, index) => { setTimeout(() => { cell.classList.remove('incorrect', 'highlighted') cell.classList.add('correct', 'word-complete') setTimeout(() => { cell.classList.remove('word-complete') }, 800) }, index * 100) }) this.showToast('Correct! 🎉') this.checkPuzzleComplete() } animateWordError(word) { const cells = [] if (word.direction === 'horizontal') { for (let c = word.startCol; c < word.startCol + word.answer.length; c++) { const cellData = this.cells.find(cell => cell.row === word.startRow && cell.col === c) if (cellData) cells.push(cellData.element) } } else { for (let r = word.startRow; r < word.startRow + word.answer.length; r++) { const cellData = this.cells.find(cell => cell.row === r && cell.col === word.startCol) if (cellData) cells.push(cellData.element) } } cells.forEach(cell => { cell.classList.remove('correct') cell.classList.add('incorrect') setTimeout(() => { cell.classList.remove('incorrect') }, 1000) }) this.showToast('Probeer opnieuw') } checkPuzzleComplete() { const allCorrect = this.cells.every(cell => { const row = cell.row const col = cell.col const key = `${row}-${col}` const userAnswer = this.userAnswers[key] const correctAnswer = this.getCorrectAnswer(row, col) return userAnswer === correctAnswer }) if (allCorrect) { setTimeout(() => this.showSuccess(), 500) } } checkAllAnswers() { this.cells.forEach(cell => { const row = cell.row const col = cell.col const key = `${row}-${col}` const userAnswer = this.userAnswers[key] const correctAnswer = this.getCorrectAnswer(row, col) if (userAnswer === correctAnswer) { cell.element.classList.add('correct') cell.element.classList.remove('incorrect') } else if (userAnswer) { cell.element.classList.add('incorrect') cell.element.classList.remove('correct') } }) this.checkPuzzleComplete() } getCorrectAnswer(row, col) { for (const word of this.currentPuzzle.words) { if (word.direction === 'horizontal') { if (word.startRow === row && col >= word.startCol && col < word.startCol + word.answer.length) { return word.answer[col - word.startCol] } } else if (word.direction === 'vertical') { if (word.startCol === col && row >= word.startRow && row < word.startRow + word.answer.length) { return word.answer[row - word.startRow] } } } return null } useHintForCurrentWord() { if (this.hints <= 0) { this.showToast('Geen hints meer!') return } if (!this.selectedWord) { this.showToast('Selecteer eerst een woord') return } const word = this.selectedWord const emptyCells = [] if (word.direction === 'horizontal') { for (let c = word.startCol; c < word.startCol + word.answer.length; c++) { const key = `${word.startRow}-${c}` if (!this.userAnswers[key]) { emptyCells.push({ row: word.startRow, col: c, letter: word.answer[c - word.startCol] }) } } } else { for (let r = word.startRow; r < word.startRow + word.answer.length; r++) { const key = `${r}-${word.startCol}` if (!this.userAnswers[key]) { emptyCells.push({ row: r, col: word.startCol, letter: word.answer[r - word.startRow] }) } } } if (emptyCells.length === 0) { this.showToast('Woord al compleet!') return } const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)] const key = `${randomCell.row}-${randomCell.col}` this.userAnswers[key] = randomCell.letter const cellData = this.cells.find(c => c.row === randomCell.row && c.col === randomCell.col) if (cellData) { cellData.element.textContent = randomCell.letter } this.hints-- this.updateUI() this.saveGameState() this.showToast('Hint gebruikt!') this.checkWordComplete() } clearGrid() { if (confirm('Alles wissen?')) { this.userAnswers = {} this.cells.forEach(cell => { cell.element.textContent = '' cell.element.classList.remove('correct', 'incorrect', 'highlighted') }) this.saveGameState() this.updateProgress() this.showToast('Gewist') } } newGame() { if (confirm('Nieuw spel starten?')) { this.currentLevel = 1 this.userAnswers = {} this.loadPuzzle(1) this.saveGameState() document.getElementById('sideMenu').classList.remove('active') this.showToast('Nieuw spel gestart!') } } showSuccess() { this.coins += 50 this.hints += 1 this.updateUI() this.saveGameState() document.getElementById('successModal').classList.add('active') } nextPuzzle() { document.getElementById('successModal').classList.remove('active') this.currentLevel++ this.userAnswers = {} this.loadPuzzle(this.currentLevel) this.saveGameState() this.showToast(`Niveau ${this.currentLevel}`) } updateProgress() { const total = this.cells.length const filled = Object.keys(this.userAnswers).length const progress = total > 0 ? Math.round((filled / total) * 100) : 0 document.getElementById('progressFill').style.width = `${progress}%` document.getElementById('progressText').textContent = `${progress}%` } updateUI() { document.getElementById('coinsCount').textContent = this.coins document.getElementById('hintsCount').textContent = this.hints document.getElementById('levelNum').textContent = this.currentLevel } showToast(message) { const toast = document.createElement('div') toast.className = 'toast' toast.textContent = message document.body.appendChild(toast) setTimeout(() => { toast.remove() }, 2500) } // LocalStorage - Game State Persistence saveGameState() { const state = { currentLevel: this.currentLevel, coins: this.coins, hints: this.hints, userAnswers: this.userAnswers, timestamp: Date.now() } localStorage.setItem('crosswordGameState', JSON.stringify(state)) } loadGameState() { const saved = localStorage.getItem('crosswordGameState') if (saved) { const state = JSON.parse(saved) this.currentLevel = state.currentLevel || 1 this.coins = state.coins || 100 this.hints = state.hints || 3 this.userAnswers = state.userAnswers || {} } } } // Initialize document.addEventListener('DOMContentLoaded', () => { new TabletCrosswordApp() })