482 lines
13 KiB
JavaScript
482 lines
13 KiB
JavaScript
// Crossword Game Controller
|
|
class CrosswordGame {
|
|
constructor() {
|
|
this.currentPuzzle = null;
|
|
this.userAnswers = {};
|
|
this.selectedCell = null;
|
|
this.gridElement = document.getElementById('crosswordGrid');
|
|
this.progress = 0;
|
|
|
|
this.initializeEventListeners();
|
|
this.loadNewPuzzle();
|
|
}
|
|
|
|
initializeEventListeners() {
|
|
// New puzzle button
|
|
document.getElementById('newPuzzleBtn').addEventListener('click', () => {
|
|
this.loadNewPuzzle();
|
|
});
|
|
|
|
// Settings button
|
|
document.getElementById('settingsBtn').addEventListener('click', () => {
|
|
document.getElementById('settingsModal').style.display = 'block';
|
|
});
|
|
|
|
// Close modal
|
|
document.querySelector('.close').addEventListener('click', () => {
|
|
document.getElementById('settingsModal').style.display = 'none';
|
|
});
|
|
|
|
// Game control buttons
|
|
document.getElementById('checkAnswersBtn').addEventListener('click', () => {
|
|
this.checkAnswers();
|
|
});
|
|
|
|
document.getElementById('revealLetterBtn').addEventListener('click', () => {
|
|
this.revealLetter();
|
|
});
|
|
|
|
document.getElementById('clearGridBtn').addEventListener('click', () => {
|
|
this.clearGrid();
|
|
});
|
|
|
|
// Settings changes
|
|
document.getElementById('difficultySelect').addEventListener('change', (e) => {
|
|
this.loadNewPuzzle(e.target.value);
|
|
});
|
|
|
|
// Keyboard navigation
|
|
document.addEventListener('keydown', (e) => {
|
|
this.handleKeyboardNavigation(e);
|
|
});
|
|
}
|
|
|
|
async loadNewPuzzle(difficulty = 'medium') {
|
|
try {
|
|
this.showLoading();
|
|
this.currentPuzzle = await PuzzleDatabase.getPuzzle(difficulty);
|
|
this.renderGrid();
|
|
this.userAnswers = {};
|
|
this.updateProgress();
|
|
} catch (error) {
|
|
console.error('Error loading puzzle:', error);
|
|
this.showError('Er is een fout opgetreden bij het laden van de puzzel.');
|
|
}
|
|
}
|
|
|
|
renderGrid() {
|
|
if (!this.currentPuzzle) return;
|
|
|
|
const grid = this.currentPuzzle.grid;
|
|
const words = this.currentPuzzle.words;
|
|
|
|
this.gridElement.innerHTML = '';
|
|
this.gridElement.style.gridTemplateColumns = `repeat(${grid[0].length}, 1fr)`;
|
|
|
|
// Create grid cells
|
|
for (let row = 0; row < grid.length; row++) {
|
|
for (let col = 0; col < grid[row].length; col++) {
|
|
const cell = grid[row][row];
|
|
const cellElement = this.createCellElement(cell, row, col, words);
|
|
this.gridElement.appendChild(cellElement);
|
|
}
|
|
}
|
|
|
|
// Add event listeners to input cells
|
|
this.attachInputListeners();
|
|
}
|
|
|
|
createCellElement(cellType, row, col, words) {
|
|
const cellDiv = document.createElement('div');
|
|
cellDiv.className = 'grid-cell';
|
|
cellDiv.dataset.row = row;
|
|
cellDiv.dataset.col = col;
|
|
|
|
switch (cellType) {
|
|
case '#':
|
|
cellDiv.classList.add('blocked-cell');
|
|
break;
|
|
case 'C':
|
|
case 'L':
|
|
case 'U':
|
|
case 'E':
|
|
// This is a clue cell (example from template)
|
|
const clueData = this.findClueForPosition(row, col, words);
|
|
if (clueData) {
|
|
cellDiv.classList.add('clue-cell', clueData.direction);
|
|
cellDiv.innerHTML = `
|
|
<div class="clue-text">${clueData.clue}</div>
|
|
<div class="arrow ${clueData.direction === 'horizontal' ? 'arrow-right' : 'arrow-down'}">
|
|
${clueData.direction === 'horizontal' ? '→' : '↓'}
|
|
</div>
|
|
`;
|
|
}
|
|
break;
|
|
default:
|
|
// This should be an input cell
|
|
cellDiv.classList.add('input-cell');
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.maxLength = 1;
|
|
input.dataset.row = row;
|
|
input.dataset.col = col;
|
|
|
|
// Add existing answer if available
|
|
const key = `${row}-${col}`;
|
|
if (this.userAnswers[key]) {
|
|
input.value = this.userAnswers[key];
|
|
}
|
|
|
|
cellDiv.appendChild(input);
|
|
}
|
|
|
|
return cellDiv;
|
|
}
|
|
|
|
findClueForPosition(row, col, words) {
|
|
for (const word of words) {
|
|
if (word.startRow === row && word.startCol === col) {
|
|
return {
|
|
clue: word.clue,
|
|
direction: word.direction
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
attachInputListeners() {
|
|
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;
|
|
|
|
// Move to next cell automatically
|
|
if (value && value.length === 1) {
|
|
this.moveToNextCell(input);
|
|
}
|
|
|
|
this.updateProgress();
|
|
}
|
|
|
|
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;
|
|
|
|
// Highlight related word
|
|
this.highlightWord(input);
|
|
}
|
|
|
|
highlightWord(input) {
|
|
// Remove previous highlights
|
|
document.querySelectorAll('.grid-cell').forEach(cell => {
|
|
cell.classList.remove('highlighted');
|
|
});
|
|
|
|
// Find and highlight the entire word
|
|
const row = parseInt(input.dataset.row);
|
|
const col = parseInt(input.dataset.col);
|
|
|
|
// This is a simplified version - in a real app you'd track word boundaries
|
|
const word = this.findWordAtPosition(row, col);
|
|
if (word) {
|
|
// Highlight all cells in this word
|
|
// Implementation depends on your word tracking system
|
|
}
|
|
}
|
|
|
|
moveToNextCell(currentInput) {
|
|
const row = parseInt(currentInput.dataset.row);
|
|
const col = parseInt(currentInput.dataset.col);
|
|
|
|
// Try to find next input cell in row first
|
|
let nextInput = null;
|
|
const currentCell = currentInput.parentElement;
|
|
const nextCell = currentCell.nextElementSibling;
|
|
|
|
if (nextCell && nextCell.querySelector('input')) {
|
|
nextInput = nextCell.querySelector('input');
|
|
} else {
|
|
// Move to next row
|
|
const currentRow = currentCell.parentElement;
|
|
const nextRow = currentRow.nextElementSibling;
|
|
if (nextRow) {
|
|
const firstInput = nextRow.querySelector('input');
|
|
if (firstInput) {
|
|
nextInput = firstInput;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nextInput) {
|
|
nextInput.focus();
|
|
nextInput.select();
|
|
}
|
|
}
|
|
|
|
handleKeyboardNavigation(e) {
|
|
if (!this.selectedCell) return;
|
|
|
|
const row = parseInt(this.selectedCell.dataset.row);
|
|
const col = parseInt(this.selectedCell.dataset.col);
|
|
|
|
let targetRow = row;
|
|
let targetCol = col;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowUp':
|
|
targetRow = Math.max(0, row - 1);
|
|
break;
|
|
case 'ArrowDown':
|
|
targetRow = Math.min(this.currentPuzzle.grid.length - 1, row + 1);
|
|
break;
|
|
case 'ArrowLeft':
|
|
targetCol = Math.max(0, col - 1);
|
|
break;
|
|
case 'ArrowRight':
|
|
targetCol = Math.min(this.currentPuzzle.grid[0].length - 1, col + 1);
|
|
break;
|
|
case 'Backspace':
|
|
this.selectedCell.value = '';
|
|
delete this.userAnswers[`${row}-${col}`];
|
|
this.updateProgress();
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
const targetInput = this.gridElement.querySelector(`input[data-row="${targetRow}"][data-col="${targetCol}"]`);
|
|
if (targetInput) {
|
|
targetInput.focus();
|
|
targetInput.select();
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
// Find the correct answer for this position
|
|
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;
|
|
|
|
// Show feedback
|
|
if (percentage === 100) {
|
|
this.showSuccess('Gefeliciteerd! Alle antwoorden zijn correct!');
|
|
} else {
|
|
this.showFeedback(`Je hebt ${correct} van de ${total} letters correct (${percentage}%).`);
|
|
}
|
|
}
|
|
|
|
findCorrectAnswer(row, col) {
|
|
// Find the correct letter for this position
|
|
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;
|
|
}
|
|
|
|
revealLetter() {
|
|
if (!this.selectedCell) {
|
|
this.showError('Selecteer eerst een cel om een letter te onthullen.');
|
|
return;
|
|
}
|
|
|
|
const row = parseInt(this.selectedCell.dataset.row);
|
|
const col = parseInt(this.selectedCell.dataset.col);
|
|
const correctAnswer = this.findCorrectAnswer(row, col);
|
|
|
|
if (correctAnswer) {
|
|
this.selectedCell.value = correctAnswer;
|
|
const key = `${row}-${col}`;
|
|
this.userAnswers[key] = correctAnswer;
|
|
this.updateProgress();
|
|
this.showFeedback('Letter onthuld!');
|
|
}
|
|
}
|
|
|
|
clearGrid() {
|
|
if (confirm('Weet je zeker dat je het hele raster 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.');
|
|
}
|
|
}
|
|
|
|
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++;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.progress = total > 0 ? Math.round((filled / total) * 100) : 0;
|
|
|
|
const progressFill = document.getElementById('progressFill');
|
|
const progressText = document.getElementById('progressText');
|
|
|
|
progressFill.style.width = `${this.progress}%`;
|
|
progressText.textContent = `${this.progress}% voltooid`;
|
|
|
|
if (this.progress === 100) {
|
|
this.checkAnswers();
|
|
}
|
|
}
|
|
|
|
showLoading() {
|
|
this.gridElement.innerHTML = '<div style="text-align: center; padding: 50px;"><div class="loading"></div><p>Laden...</p></div>';
|
|
}
|
|
|
|
showError(message) {
|
|
this.showFeedback(message, 'error');
|
|
}
|
|
|
|
showSuccess(message) {
|
|
this.showFeedback(message, 'success');
|
|
}
|
|
|
|
showFeedback(message, type = 'info') {
|
|
// Create feedback element
|
|
const feedback = document.createElement('div');
|
|
feedback.className = `feedback feedback-${type}`;
|
|
feedback.textContent = message;
|
|
|
|
// Style the feedback
|
|
feedback.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
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;
|
|
`;
|
|
|
|
// Set background color based on type
|
|
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);
|
|
|
|
// Remove after 3 seconds
|
|
setTimeout(() => {
|
|
feedback.style.animation = 'slideOut 0.3s ease';
|
|
setTimeout(() => {
|
|
if (feedback.parentNode) {
|
|
feedback.parentNode.removeChild(feedback);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
// Add CSS animations for feedback
|
|
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; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// Initialize the game when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const game = new CrosswordGame();
|
|
});
|