// Kruiswoord Pro Game Logic - Fixed Version with Tablet Support and Correct Hints class KruiswoordProGame { constructor() { this.currentLevel = 1 this.coins = 100 this.hints = 3 this.currentPuzzle = null this.userAnswers = {} this.selectedCell = null this.gridElement = document.getElementById('crosswordGrid') this.isTablet = window.innerWidth >= 768 this.initializeGame() this.setupEventListeners() this.loadPuzzle(this.currentLevel) // Handle window resize for tablet/desktop window.addEventListener('resize', () => { this.handleResize() }) } handleResize() { const wasTablet = this.isTablet this.isTablet = window.innerWidth >= 768 if (wasTablet !== this.isTablet && this.currentPuzzle) { this.renderGrid() // Re-render for different screen size } } initializeGame() { this.updateUI() this.setupTabletStyles() } setupTabletStyles() { if (this.isTablet) { document.body.classList.add('tablet-mode') } else { document.body.classList.remove('tablet-mode') } } setupEventListeners() { // Menu controls document.getElementById('menuBtn').addEventListener('click', () => { document.getElementById('sideMenu').classList.add('active') }) document.getElementById('closeMenu').addEventListener('click', () => { document.getElementById('sideMenu').classList.remove('active') }) // Game controls document.getElementById('checkBtn').addEventListener('click', () => { this.checkAnswers() }) document.getElementById('clearBtn').addEventListener('click', () => { this.clearGrid() }) document.getElementById('revealLetterBtn').addEventListener('click', () => { this.showHintModal() }) document.getElementById('skipBtn').addEventListener('click', () => { this.skipPuzzle() }) // Hint modal document.getElementById('useHintBtn').addEventListener('click', () => { this.useHint() }) document.getElementById('cancelHintBtn').addEventListener('click', () => { this.hideHintModal() }) // Success modal document.getElementById('nextPuzzleBtn').addEventListener('click', () => { this.nextPuzzle() }) document.getElementById('closeModalBtn').addEventListener('click', () => { this.hideSuccessModal() }) // Menu items document.getElementById('dailyPuzzle').addEventListener('click', () => { this.loadDailyPuzzle() }) document.getElementById('levelSelect').addEventListener('click', () => { this.showLevelSelect() }) // Keyboard navigation document.addEventListener('keydown', (e) => { this.handleKeyboardInput(e) }) } // Load puzzles from external puzzleData.js getDutchPuzzles() { // Use puzzles from puzzleData.js if available if (typeof DUTCH_PUZZLES !== 'undefined') { return { 1: DUTCH_PUZZLES.level1, 2: DUTCH_PUZZLES.level2, 3: DUTCH_PUZZLES.level3 } } // Fallback puzzles return { 1: { grid : [ ['H', 'A', 'A', 'S'], ['#', '#', '#', '#'], ['V', 'L', 'I', 'E', 'G'], ['#', '#', '#', '#', '#'] ], words : [ { word : 'HAAS', clue : 'Snel dier', startRow : 0, startCol : 0, direction: 'horizontal', answer : 'HAAS' }, { word : 'VLIEG', clue : 'Insect', startRow : 2, startCol : 0, direction: 'horizontal', answer : 'VLIEG' } ], difficulty: 1 }, 2: { grid : [ ['B', 'O', 'M', 'E', 'N'], ['#', '#', '#', '#', '#'], ['H', 'O', 'N', 'D', 'E'], ['#', '#', '#', '#', '#'] ], words : [ { word : 'BOMEN', clue : 'Planten →', startRow : 0, startCol : 0, direction: 'horizontal', answer : 'BOMEN' }, { word : 'HONDE', clue : 'Huisdieren →', startRow : 2, startCol : 0, direction: 'horizontal', answer : 'HONDE' } ], difficulty: 2 }, 3: { grid : [ ['R', 'E', 'G', 'E', 'N'], ['#', '#', '#', '#', '#'], ['S', 'N', 'E', 'E', 'U'], ['#', '#', '#', '#', '#'] ], words : [ { word : 'REGEN', clue : 'Valt uit de lucht →', startRow : 0, startCol : 0, direction: 'horizontal', answer : 'REGEN' }, { word : 'SNEEU', clue : 'Witte vlokken →', startRow : 2, startCol : 0, direction: 'horizontal', answer : 'SNEEU' } ], difficulty: 3 }, // Tablet-optimized larger puzzles 4: { grid : [ ['H', 'A', 'A', 'S', '#', '#'], ['#', '#', '#', 'T', '#', '#'], ['V', 'L', 'I', 'E', 'G', '#'], ['#', '#', '#', 'S', '#', '#'], ['#', '#', '#', '#', '#', '#'], ['#', '#', '#', '#', '#', '#'] ], words : [ { word : 'HAAS', clue : 'Snel dier →', startRow : 0, startCol : 0, direction: 'horizontal', answer : 'HAAS' }, { word : 'VLIEG', clue : 'Insect', startRow : 2, startCol : 0, direction: 'horizontal', answer : 'VLIEG' } ], difficulty : 2, tabletOptimized: true } } } loadPuzzle(level) { const puzzles = this.getDutchPuzzles() this.currentPuzzle = puzzles[level] || puzzles[1] this.renderGrid() this.userAnswers = {} this.updateProgress() } calculateOptimalCellSize(rows, cols) { // Get available viewport space with mobile-specific adjustments const isMobile = window.innerWidth < 768 const headerHeight = isMobile ? 70 : 80 const progressHeight = isMobile ? 50 : 60 const controlsHeight = isMobile ? 100 : 120 const gridPadding = isMobile ? 5 : 10 // padding inside grid-container const sectionPadding = isMobile ? 5 : 20 // padding of puzzle-section const mainPadding = isMobile ? 10 : 20 // game-main padding const totalPadding = (gridPadding * 2) + (sectionPadding * 2) + (mainPadding * 2) + (isMobile ? 20 : 0) const gap = isMobile ? 1 : 2 const availableHeight = window.innerHeight - headerHeight - progressHeight - controlsHeight - totalPadding const availableWidth = window.innerWidth - totalPadding // Calculate max cell size based on available space // Account for gaps between cells const totalGapHeight = (rows - 1) * gap const totalGapWidth = (cols - 1) * gap const maxCellFromHeight = (availableHeight - totalGapHeight) / rows const maxCellFromWidth = (availableWidth - totalGapWidth) / cols // Use the smaller dimension let cellSize = Math.floor(Math.min(maxCellFromHeight, maxCellFromWidth)) // Set min/max bounds based on device - more conservative on mobile const minSize = isMobile ? 28 : 40 const maxSize = isMobile ? 45 : 120 cellSize = Math.max(minSize, Math.min(maxSize, cellSize)) console.log('Grid calculation:', { isMobile, viewportWidth: window.innerWidth, availableWidth, availableHeight, rows, cols, maxCellFromWidth, maxCellFromHeight, finalCellSize: cellSize, totalGridWidth: cols * cellSize + totalGapWidth + (gridPadding * 2), totalPadding }) return cellSize } renderGrid() { if (!this.currentPuzzle) return const grid = this.currentPuzzle.grid const words = this.currentPuzzle.words this.gridElement.innerHTML = '' // Set grid dimensions - add 1 column for clue cells on the left const rows = grid.length const cols = Math.max(...grid.map(row => row.length)) + 1 // Calculate available space and determine optimal cell size const cellSize = this.calculateOptimalCellSize(rows, cols) const isMobile = window.innerWidth < 768 const gap = isMobile ? 1 : 2 this.gridElement.style.gridTemplateColumns = `repeat(${ cols }, ${ cellSize }px)` this.gridElement.style.gridTemplateRows = `repeat(${ rows }, ${ cellSize }px)` this.gridElement.style.gap = `${gap}px` this.gridElement.style.justifyContent = 'center' this.gridElement.style.margin = '0 auto' // Create grid cells with clue column for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { if (col === 0) { // First column is for clues const clueData = this.getClueForRow(row, words) const cellElement = this.createClueCell(row, clueData, cellSize) this.gridElement.appendChild(cellElement) } else { // Remaining columns are the actual grid (offset by 1) const gridCol = col - 1 const cellValue = grid[row] && grid[row][gridCol] ? grid[row][gridCol] : '#' const cellElement = this.createCellElement(cellValue, row, gridCol, words, cellSize) this.gridElement.appendChild(cellElement) } } } this.attachCellListeners() } getClueForRow(row, words) { // Find horizontal word that starts in this row for (const word of words) { if (word.direction === 'horizontal' && word.startRow === row) { return { clue: word.clue, direction: 'horizontal' } } } return null } createClueCell(row, clueData, cellSize) { const cellDiv = document.createElement('div') cellDiv.className = 'grid-cell clue-cell' cellDiv.style.width = `${ cellSize }px` cellDiv.style.height = `${ cellSize }px` if (clueData) { // Has a clue for this row const clueText = document.createElement('div') clueText.className = 'clue-text' clueText.textContent = clueData.clue // Responsive font sizing based on cell size const fontSize = Math.max(7, Math.min(12, cellSize / 6)) clueText.style.fontSize = `${fontSize}px` clueText.style.lineHeight = '1.1' clueText.style.padding = '2px' const arrow = document.createElement('div') arrow.className = 'arrow arrow-right' arrow.textContent = '→' const arrowSize = Math.max(12, Math.min(24, cellSize / 3)) arrow.style.fontSize = `${arrowSize}px` cellDiv.appendChild(clueText) cellDiv.appendChild(arrow) } else { // Empty clue cell cellDiv.classList.add('blocked-cell') } return cellDiv } createCellElement(cellValue, row, col, words, cellSize) { const cellDiv = document.createElement('div') cellDiv.className = 'grid-cell' cellDiv.dataset.row = row cellDiv.dataset.col = col // FIXED: Set consistent sizing cellDiv.style.width = `${ cellSize }px` cellDiv.style.height = `${ cellSize }px` if (cellValue === '#') { // Check if this is a clue cell (before second word in row) const clueData = this.getClueForPosition(row, col, words) if (clueData) { // This is a clue cell for the second word cellDiv.classList.add('clue-cell') const clueText = document.createElement('div') clueText.className = 'clue-text' clueText.textContent = clueData.clue // Responsive font sizing based on cell size const fontSize = Math.max(7, Math.min(12, cellSize / 6)) clueText.style.fontSize = `${fontSize}px` clueText.style.lineHeight = '1.1' clueText.style.padding = '2px' const arrow = document.createElement('div') arrow.className = 'arrow arrow-right' arrow.textContent = '→' const arrowSize = Math.max(12, Math.min(24, cellSize / 3)) arrow.style.fontSize = `${arrowSize}px` cellDiv.appendChild(clueText) cellDiv.appendChild(arrow) } else { // Regular blocked cell cellDiv.classList.add('blocked-cell') } } else { // Letter cell - create input if (this.isPartOfWord(row, col, words)) { cellDiv.classList.add('input-cell') const input = document.createElement('input') input.type = 'text' input.maxLength = 1 input.dataset.row = row input.dataset.col = col // Responsive font sizing based on cell size const inputFontSize = Math.max(16, Math.min(36, cellSize * 0.5)) input.style.fontSize = `${inputFontSize}px` // Add existing answer if available const key = `${ row }-${ col }` if (this.userAnswers[key]) { input.value = this.userAnswers[key] } cellDiv.appendChild(input) } else { // Empty cell that's not part of any word cellDiv.classList.add('blocked-cell') } } return cellDiv } // FIXED: Check if cell is part of any word isPartOfWord(row, col, words) { for (const word of words) { if (word.direction === 'horizontal') { if (row === word.startRow && col >= word.startCol && col < word.startCol + word.answer.length) { return true } } else if (word.direction === 'vertical') { if (col === word.startCol && row >= word.startRow && row < word.startRow + word.answer.length) { return true } } } return false } getClueForPosition(row, col, words) { for (const word of words) { if (word.direction === 'horizontal') { // Horizontal clue appears one cell to the left of word start if (word.startRow === row && word.startCol - 1 === col) { return { clue : word.clue, direction: word.direction } } } else if (word.direction === 'vertical') { // Vertical clue appears one cell above word start if (word.startRow - 1 === row && word.startCol === col) { return { clue : word.clue, direction: word.direction } } } } return null } attachCellListeners() { const inputs = this.gridElement.querySelectorAll('input') inputs.forEach(input => { input.addEventListener('input', (e) => { this.handleInput(e) }) input.addEventListener('focus', (e) => { this.selectCell(e.target) }) input.addEventListener('click', (e) => { this.selectCell(e.target) }) }) } handleInput(e) { const input = e.target const value = input.value.toUpperCase() input.value = value const row = parseInt(input.dataset.row) const col = parseInt(input.dataset.col) const key = `${ row }-${ col }` this.userAnswers[key] = value // FIXED: Smart navigation to next cell in the same word if (value && value.length === 1) { this.moveToNextInWord(input, row, col) } this.updateProgress() } // FIXED: Navigate to next cell in the same word, not just any next input moveToNextInWord(currentInput, row, col) { const currentWord = this.findWordContainingCell(row, col) if (!currentWord) { this.moveToNextCell(currentInput) return } let nextRow = row let nextCol = col if (currentWord.direction === 'horizontal') { nextCol++ } else { nextRow++ } // Check if next position is still in the same word const nextInput = this.gridElement.querySelector(`input[data-row="${ nextRow }"][data-col="${ nextCol }"]`) if (nextInput && this.isPartOfWord(nextRow, nextCol, this.currentPuzzle.words)) { nextInput.focus() nextInput.select() } else { // End of word, move to next available input this.moveToNextCell(currentInput) } } findWordContainingCell(row, col) { for (const word of this.currentPuzzle.words) { if (word.direction === 'horizontal') { if (row === word.startRow && col >= word.startCol && col < word.startCol + word.answer.length) { return word } } else if (word.direction === 'vertical') { if (col === word.startCol && row >= word.startRow && row < word.startRow + word.answer.length) { return word } } } return null } selectCell(input) { // Remove previous selection document.querySelectorAll('.grid-cell').forEach(cell => { cell.classList.remove('active') }) // Add selection to current cell input.parentElement.classList.add('active') this.selectedCell = input } // FIXED: Better navigation that respects word boundaries moveToNextCell(currentInput) { const inputs = Array.from(this.gridElement.querySelectorAll('input')) const currentIndex = inputs.indexOf(currentInput) if (currentIndex !== -1 && currentIndex < inputs.length - 1) { const nextInput = inputs[currentIndex + 1] nextInput.focus() nextInput.select() } } handleKeyboardInput(e) { if (!this.selectedCell) return const inputs = Array.from(this.gridElement.querySelectorAll('input')) const currentIndex = inputs.indexOf(this.selectedCell) if (currentIndex === -1) return let targetIndex = currentIndex switch (e.key) { case 'ArrowUp': const currentRow = parseInt(this.selectedCell.dataset.row) const currentCol = parseInt(this.selectedCell.dataset.col) const targetInput = this.findInputAbove(currentRow, currentCol) if (targetInput) { targetInput.focus() targetInput.select() } break case 'ArrowDown': const targetInputDown = this.findInputBelow(currentRow, currentCol) if (targetInputDown) { targetInputDown.focus() targetInputDown.select() } break case 'ArrowLeft': if (currentIndex > 0) { targetIndex-- inputs[targetIndex].focus() inputs[targetIndex].select() } break case 'ArrowRight': if (currentIndex < inputs.length - 1) { targetIndex++ inputs[targetIndex].focus() inputs[targetIndex].select() } break case 'Backspace': this.selectedCell.value = '' const row = parseInt(this.selectedCell.dataset.row) const col = parseInt(this.selectedCell.dataset.col) delete this.userAnswers[`${ row }-${ col }`] this.updateProgress() break } } findInputAbove(row, col) { for (let r = row - 1; r >= 0; r--) { const input = this.gridElement.querySelector(`input[data-row="${ r }"][data-col="${ col }"]`) if (input) return input } return null } findInputBelow(row, col) { const maxRow = Math.max(...Array.from(this.gridElement.querySelectorAll('input')).map(input => parseInt(input.dataset.row))) for (let r = row + 1; r <= maxRow; r++) { const input = this.gridElement.querySelector(`input[data-row="${ r }"][data-col="${ col }"]`) if (input) return input } return null } checkAnswers() { let correct = 0 let total = 0 const inputs = this.gridElement.querySelectorAll('input') inputs.forEach(input => { const row = parseInt(input.dataset.row) const col = parseInt(input.dataset.col) const key = `${ row }-${ col }` const userAnswer = input.value.toUpperCase() const correctAnswer = this.findCorrectAnswer(row, col) if (correctAnswer) { total++ if (userAnswer === correctAnswer) { correct++ input.parentElement.classList.add('correct') input.parentElement.classList.remove('incorrect') } else if (userAnswer) { input.parentElement.classList.add('incorrect') input.parentElement.classList.remove('correct') } } }) const percentage = total > 0 ? Math.round((correct / total) * 100) : 0 if (percentage === 100) { this.showSuccess() } else { this.showFeedback(`Je hebt ${ correct } van de ${ total } letters correct (${ percentage }%)`) } } findCorrectAnswer(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) { const letterIndex = col - word.startCol return word.answer[letterIndex] } } else if (word.direction === 'vertical') { if (word.startCol === col && row >= word.startRow && row < word.startRow + word.answer.length) { const letterIndex = row - word.startRow return word.answer[letterIndex] } } } return null } clearGrid() { if (confirm('Weet je zeker dat je alle ingevulde letters wilt wissen?')) { const inputs = this.gridElement.querySelectorAll('input') inputs.forEach(input => { input.value = '' input.parentElement.classList.remove('correct', 'incorrect') }) this.userAnswers = {} this.updateProgress() this.showFeedback('Raster gewist') } } showHintModal() { if (this.hints <= 0) { this.showFeedback('Je hebt geen hints meer. Verdien hints door puzzels op te lossen!', 'error') return } document.getElementById('hintModal').classList.add('active') } hideHintModal() { document.getElementById('hintModal').classList.remove('active') } useHint() { if (this.hints <= 0) return const inputs = this.gridElement.querySelectorAll('input') const emptyInputs = Array.from(inputs).filter(input => !input.value) if (emptyInputs.length === 0) { this.showFeedback('Alle letters zijn al ingevuld!', 'info') this.hideHintModal() return } // Randomly select an empty cell const randomInput = emptyInputs[Math.floor(Math.random() * emptyInputs.length)] const row = parseInt(randomInput.dataset.row) const col = parseInt(randomInput.dataset.col) const correctAnswer = this.findCorrectAnswer(row, col) if (correctAnswer) { randomInput.value = correctAnswer const key = `${ row }-${ col }` this.userAnswers[key] = correctAnswer this.hints-- this.updateUI() this.updateProgress() this.showFeedback('Letter onthuld!') this.hideHintModal() // Check if puzzle is complete this.checkIfComplete() } } checkIfComplete() { const inputs = this.gridElement.querySelectorAll('input') let allFilled = true inputs.forEach(input => { if (!input.value) { allFilled = false } }) if (allFilled) { setTimeout(() => this.checkAnswers(), 500) } } showSuccess() { // Award rewards this.coins += 50 this.hints += 1 this.updateUI() document.getElementById('successModal').classList.add('active') } hideSuccessModal() { document.getElementById('successModal').classList.remove('active') } nextPuzzle() { this.hideSuccessModal() this.currentLevel++ this.loadPuzzle(this.currentLevel) this.showFeedback(`Niveau ${ this.currentLevel } geladen!`) } skipPuzzle() { if (confirm('Weet je zeker dat je deze puzzel wilt overslaan?')) { this.currentLevel++ this.loadPuzzle(this.currentLevel) this.showFeedback('Puzzel overgeslagen') } } loadDailyPuzzle() { this.showFeedback('Dagelijkse puzzel wordt geladen...') // Implement daily puzzle logic document.getElementById('sideMenu').classList.remove('active') } showLevelSelect() { this.showFeedback('Niveau selectie komt binnenkort!') document.getElementById('sideMenu').classList.remove('active') } updateProgress() { const inputs = this.gridElement.querySelectorAll('input') let filled = 0 let total = 0 inputs.forEach(input => { const row = parseInt(input.dataset.row) const col = parseInt(input.dataset.col) const correctAnswer = this.findCorrectAnswer(row, col) if (correctAnswer) { total++ if (input.value) { filled++ } } }) const progress = total > 0 ? Math.round((filled / total) * 100) : 0 document.getElementById('progressFill').style.width = `${ progress }%` document.getElementById('progressText').textContent = `${ progress }% voltooid` } updateUI() { document.getElementById('coinsCount').textContent = this.coins document.querySelector('.hint-count').textContent = this.hints document.querySelector('.level-number').textContent = `Niveau ${ this.currentLevel }` } showFeedback(message, type = 'info') { const feedback = document.createElement('div') feedback.className = `feedback feedback-${ type }` feedback.textContent = message feedback.style.cssText = ` position: fixed; top: 100px; right: 20px; padding: 15px 20px; border-radius: 8px; color: white; font-weight: 600; z-index: 1000; animation: slideIn 0.3s ease; max-width: 300px; word-wrap: break-word; ` switch (type) { case 'error': feedback.style.background = 'linear-gradient(135deg, #f56565, #e53e3e)' break case 'success': feedback.style.background = 'linear-gradient(135deg, #48bb78, #38a169)' break default: feedback.style.background = 'linear-gradient(135deg, #4299e1, #3182ce)' } document.body.appendChild(feedback) setTimeout(() => { feedback.style.animation = 'slideOut 0.3s ease' setTimeout(() => { if (feedback.parentNode) { feedback.parentNode.removeChild(feedback) } }, 300) }, 3000) } } // Add CSS animations and tablet styles const style = document.createElement('style') style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } /* Tablet Mode Styles */ .tablet-mode .grid-container { max-width: 90vw; margin: 0 auto; } .tablet-mode .puzzle-section { padding: 30px; } @media (min-width: 768px) { .app-container { max-width: 100vw; } .game-main { padding: 30px; } .bottom-controls { max-width: 600px; margin: 0 auto; } } @media (min-width: 1024px) { .grid-container { transform: scale(1.1); transform-origin: center; } .puzzle-section { min-height: 70vh; } } ` document.head.appendChild(style) // Initialize game when DOM is loaded document.addEventListener('DOMContentLoaded', () => { const game = new KruiswoordProGame() })