658 lines
19 KiB
JavaScript
658 lines
19 KiB
JavaScript
// 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()
|
|
})
|