diff --git a/Dockerfile.desktop b/Dockerfile.desktop new file mode 100644 index 0000000..b84444c --- /dev/null +++ b/Dockerfile.desktop @@ -0,0 +1,9 @@ +# Simple static site image: serve the contents of /public with nginx +FROM nginx:alpine +WORKDIR /usr/share/nginx/html + +# Copy static files +COPY desktop/ ./ + +# Provide custom nginx.conf for clean URLs +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/Dockerfile.mobile b/Dockerfile.mobile new file mode 100644 index 0000000..cbcd190 --- /dev/null +++ b/Dockerfile.mobile @@ -0,0 +1,9 @@ +# Simple static site image: serve the contents of /public with nginx +FROM nginx:alpine +WORKDIR /usr/share/nginx/html + +# Copy static files +COPY mobile/ ./ + +# Provide custom nginx.conf for clean URLs +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/Dockerfile.tablet b/Dockerfile.tablet new file mode 100644 index 0000000..48558ac --- /dev/null +++ b/Dockerfile.tablet @@ -0,0 +1,9 @@ +# Simple static site image: serve the contents of /public with nginx +FROM nginx:alpine +WORKDIR /usr/share/nginx/html + +# Copy static files +COPY tablet/ ./ + +# Provide custom nginx.conf for clean URLs +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/public/404.html b/desktop/404.html similarity index 100% rename from public/404.html rename to desktop/404.html diff --git a/public/favicon.ico b/desktop/favicon.ico similarity index 100% rename from public/favicon.ico rename to desktop/favicon.ico diff --git a/public/game.js b/desktop/game.js similarity index 100% rename from public/game.js rename to desktop/game.js diff --git a/public/icon.png b/desktop/icon.png similarity index 100% rename from public/icon.png rename to desktop/icon.png diff --git a/public/icon.svg b/desktop/icon.svg similarity index 100% rename from public/icon.svg rename to desktop/icon.svg diff --git a/public/index.html b/desktop/index.html similarity index 99% rename from public/index.html rename to desktop/index.html index 3c67b4f..0ce6822 100644 --- a/public/index.html +++ b/desktop/index.html @@ -4,7 +4,6 @@ Kruiswoord Pro: Zweedse puzzel - diff --git a/public/puzzleData.js b/desktop/puzzleData.js similarity index 100% rename from public/puzzleData.js rename to desktop/puzzleData.js diff --git a/public/styles_v5.css b/desktop/styles_v5.css similarity index 100% rename from public/styles_v5.css rename to desktop/styles_v5.css diff --git a/docker-compose.yml b/docker-compose.yml index fdda6c1..4d19e88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,129 @@ services: - word: + # 1) Desktop site + puzzle-desk: build: - context: /opt/apps/word - dockerfile: Dockerfile - container_name: word + context: /opt/apps/puzzle + dockerfile: Dockerfile.desktop + container_name: puzzle-desk restart: unless-stopped - networks: - - traefik_net - - default + networks: [ traefik_net ] + volumes: + - /opt/apps/puzzle/desk:/usr/share/nginx/html:ro labels: - "traefik.enable=true" - - "traefik.http.routers.word.rule=Host(`word.appmodel.nl`)" - - "traefik.http.routers.word.entrypoints=websecure" - - "traefik.http.routers.word.tls=true" - - "traefik.http.services.word.loadbalancer.server.port=80" - - "traefik.http.routers.word-http.rule=Host(`word.appmodel.nl`)" - - "traefik.http.routers.word-http.entrypoints=web" - - "traefik.http.routers.word-http.middlewares=word-https" - - "traefik.http.middlewares.word-https.redirectscheme.scheme=https" - - "traefik.http.routers.auction.tls.certresolver=letsencrypt" - - "traefik.http.middlewares.word-https.redirectscheme.permanent=true" + - "traefik.http.routers.puzzle-desk.rule=Host(`desk-puzzle.appmodel.nl`)" + - "traefik.http.routers.puzzle-desk.entrypoints=websecure" + - "traefik.http.routers.puzzle-desk.tls=true" + - "traefik.http.routers.puzzle-desk.tls.certresolver=letsencrypt" + - "traefik.http.services.puzzle-desk.loadbalancer.server.port=80" + - "traefik.http.routers.puzzle-desk-http.rule=Host(`desk-puzzle.appmodel.nl`)" + - "traefik.http.routers.puzzle-desk-http.entrypoints=web" + - "traefik.http.routers.puzzle-desk-http.middlewares=redirect-to-https@file" + + # 2) Tablet site + puzzle-tab: + build: + context: /opt/apps/puzzle + dockerfile: Dockerfile.tablet + container_name: puzzle-tab + restart: unless-stopped + networks: [ traefik_net ] + volumes: + - /opt/apps/puzzle/tab:/usr/share/nginx/html:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.puzzle-tab.rule=Host(`tab-puzzle.appmodel.nl`)" + - "traefik.http.routers.puzzle-tab.entrypoints=websecure" + - "traefik.http.routers.puzzle-tab.tls=true" + - "traefik.http.routers.puzzle-tab.tls.certresolver=letsencrypt" + - "traefik.http.services.puzzle-tab.loadbalancer.server.port=80" + - "traefik.http.routers.puzzle-tab-http.rule=Host(`tab-puzzle.appmodel.nl`)" + - "traefik.http.routers.puzzle-tab-http.entrypoints=web" + - "traefik.http.routers.puzzle-tab-http.middlewares=redirect-to-https@file" + + # 3) Mobile site + puzzle-mobile: + build: + context: /opt/apps/puzzle + dockerfile: Dockerfile.mobile + container_name: puzzle-mobile + restart: unless-stopped + networks: [ traefik_net ] + volumes: + - /opt/apps/puzzle/mobile:/usr/share/nginx/html:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.puzzle-mobile.rule=Host(`mobile-puzzle.appmodel.nl`)" + - "traefik.http.routers.puzzle-mobile.entrypoints=websecure" + - "traefik.http.routers.puzzle-mobile.tls=true" + - "traefik.http.routers.puzzle-mobile.tls.certresolver=letsencrypt" + - "traefik.http.services.puzzle-mobile.loadbalancer.server.port=80" + - "traefik.http.routers.puzzle-mobile-http.rule=Host(`mobile-puzzle.appmodel.nl`)" + - "traefik.http.routers.puzzle-mobile-http.entrypoints=web" + - "traefik.http.routers.puzzle-mobile-http.middlewares=redirect-to-https@file" + + # 4) Entry domain: puzzle.appmodel.nl -> redirect to the right subdomain by User-Agent + # (this container never really serves content; it just hosts the redirect routers/middlewares) + puzzle-entry: + build: + context: /opt/apps/puzzle + dockerfile: Dockerfile + container_name: puzzle-entry + restart: unless-stopped + networks: [ traefik_net ] + labels: + - "traefik.enable=true" + + # Redirect middlewares (302 while testing; switch to true for 301) + - "traefik.http.middlewares.pz-redir-mobile.redirectregex.regex=^https?://puzzle\\.appmodel\\.nl/(.*)" + - "traefik.http.middlewares.pz-redir-mobile.redirectregex.replacement=https://mobile-puzzle.appmodel.nl/$$1" + - "traefik.http.middlewares.pz-redir-mobile.redirectregex.permanent=false" + + - "traefik.http.middlewares.pz-redir-tab.redirectregex.regex=^https?://puzzle\\.appmodel\\.nl/(.*)" + - "traefik.http.middlewares.pz-redir-tab.redirectregex.replacement=https://tab-puzzle.appmodel.nl/$$1" + - "traefik.http.middlewares.pz-redir-tab.redirectregex.permanent=false" + + - "traefik.http.middlewares.pz-redir-desk.redirectregex.regex=^https?://puzzle\\.appmodel\\.nl/(.*)" + - "traefik.http.middlewares.pz-redir-desk.redirectregex.replacement=https://desk-puzzle.appmodel.nl/$$1" + - "traefik.http.middlewares.pz-redir-desk.redirectregex.permanent=false" + + # HTTPS routers (priority matters) + - "traefik.http.routers.pz-mobile.rule=Host(`puzzle.appmodel.nl`) && HeadersRegexp(`User-Agent`,`(?i)(mobi|iphone|ipod|windows phone|android.*mobile)`)" + - "traefik.http.routers.pz-mobile.entrypoints=websecure" + - "traefik.http.routers.pz-mobile.priority=100" + - "traefik.http.routers.pz-mobile.middlewares=pz-redir-mobile" + - "traefik.http.routers.pz-mobile.tls=true" + - "traefik.http.routers.pz-mobile.tls.certresolver=letsencrypt" + + - "traefik.http.routers.pz-tab.rule=Host(`puzzle.appmodel.nl`) && HeadersRegexp(`User-Agent`,`(?i)(ipad|tablet|kindle|silk|android(?!.*mobile))`)" + - "traefik.http.routers.pz-tab.entrypoints=websecure" + - "traefik.http.routers.pz-tab.priority=90" + - "traefik.http.routers.pz-tab.middlewares=pz-redir-tab" + - "traefik.http.routers.pz-tab.tls=true" + - "traefik.http.routers.pz-tab.tls.certresolver=letsencrypt" + + - "traefik.http.routers.pz-desk.rule=Host(`puzzle.appmodel.nl`)" + - "traefik.http.routers.pz-desk.entrypoints=websecure" + - "traefik.http.routers.pz-desk.priority=10" + - "traefik.http.routers.pz-desk.middlewares=pz-redir-desk" + - "traefik.http.routers.pz-desk.tls=true" + - "traefik.http.routers.pz-desk.tls.certresolver=letsencrypt" + + # Optional: HTTP routers too (prevents extra hop via your http-catchall) + - "traefik.http.routers.pz-mobile-http.rule=Host(`puzzle.appmodel.nl`) && HeadersRegexp(`User-Agent`,`(?i)(mobi|iphone|ipod|windows phone|android.*mobile)`)" + - "traefik.http.routers.pz-mobile-http.entrypoints=web" + - "traefik.http.routers.pz-mobile-http.priority=100" + - "traefik.http.routers.pz-mobile-http.middlewares=pz-redir-mobile" + + - "traefik.http.routers.pz-tab-http.rule=Host(`puzzle.appmodel.nl`) && HeadersRegexp(`User-Agent`,`(?i)(ipad|tablet|kindle|silk|android(?!.*mobile))`)" + - "traefik.http.routers.pz-tab-http.entrypoints=web" + - "traefik.http.routers.pz-tab-http.priority=90" + - "traefik.http.routers.pz-tab-http.middlewares=pz-redir-tab" + + - "traefik.http.routers.pz-desk-http.rule=Host(`puzzle.appmodel.nl`)" + - "traefik.http.routers.pz-desk-http.entrypoints=web" + - "traefik.http.routers.pz-desk-http.priority=10" + - "traefik.http.routers.pz-desk-http.middlewares=pz-redir-desk" networks: traefik_net: diff --git a/mobile/404.html b/mobile/404.html new file mode 100644 index 0000000..260cc4c --- /dev/null +++ b/mobile/404.html @@ -0,0 +1,62 @@ + + + + + + Page Not Found + + + + + +

Page Not Found

+

Sorry, but the page you were trying to view does not exist.

+ + + + diff --git a/mobile/favicon.ico b/mobile/favicon.ico new file mode 100644 index 0000000..be74abd Binary files /dev/null and b/mobile/favicon.ico differ diff --git a/mobile/game.js b/mobile/game.js new file mode 100644 index 0000000..817bcd6 --- /dev/null +++ b/mobile/game.js @@ -0,0 +1,954 @@ +// 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() +}) diff --git a/mobile/icon.png b/mobile/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/mobile/icon.png differ diff --git a/mobile/icon.svg b/mobile/icon.svg new file mode 100644 index 0000000..f232922 --- /dev/null +++ b/mobile/icon.svg @@ -0,0 +1 @@ + diff --git a/public/mobile.html b/mobile/index.html similarity index 98% rename from public/mobile.html rename to mobile/index.html index 044e042..80765a3 100644 --- a/public/mobile.html +++ b/mobile/index.html @@ -4,7 +4,6 @@ Kruiswoord Pro - Mobile - diff --git a/public/mobile.css b/mobile/mobile.css similarity index 100% rename from public/mobile.css rename to mobile/mobile.css diff --git a/public/mobile.js b/mobile/mobile.js similarity index 100% rename from public/mobile.js rename to mobile/mobile.js diff --git a/mobile/puzzleData.js b/mobile/puzzleData.js new file mode 100644 index 0000000..d6a8e93 --- /dev/null +++ b/mobile/puzzleData.js @@ -0,0 +1,244 @@ +// Level 4 - 9x8 (Nature & Daily Words) +const level1 = { + grid: [ + ['B','O','M','E','N','#','Z','O','N'], + ['H','A','A','S','#','D','O','R','P'], + ['H','O','N','D','#','O','P','E','N'], + ['W','I','N','D','#','N','E','S','T'], + ['W','O','L','K','#','D','U','I','F'], + ['S','T','E','R','#','E','E','N','D'], + ['M','A','A','N','#','R','E','U','S'], + ['R','E','G','E','N','#','V','I','S'] + ], + words: [ + // horizontaal + { word: 'BOMEN', clue: 'Planten', startRow: 0, startCol: 0, direction: 'horizontal', answer: 'BOMEN' }, + { word: 'ZON', clue: 'Ster aan de hemel', startRow: 0, startCol: 6, direction: 'horizontal', answer: 'ZON' }, + + { word: 'HAAS', clue: 'Snel dier', startRow: 1, startCol: 0, direction: 'horizontal', answer: 'HAAS' }, + { word: 'DORP', clue: 'Kleine plaats', startRow: 1, startCol: 5, direction: 'horizontal', answer: 'DORP' }, + + { word: 'HOND', clue: 'Huisdier', startRow: 2, startCol: 0, direction: 'horizontal', answer: 'HOND' }, + { word: 'OPEN', clue: 'Niet dicht', startRow: 2, startCol: 5, direction: 'horizontal', answer: 'OPEN' }, + + { word: 'WIND', clue: 'Luchtbeweging', startRow: 3, startCol: 0, direction: 'horizontal', answer: 'WIND' }, + { word: 'NEST', clue: 'Thuis van een vogel', startRow: 3, startCol: 5, direction: 'horizontal', answer: 'NEST' }, + + { word: 'WOLK', clue: 'In de lucht', startRow: 4, startCol: 0, direction: 'horizontal', answer: 'WOLK' }, + { word: 'DUIF', clue: 'Stadsvogel', startRow: 4, startCol: 5, direction: 'horizontal', answer: 'DUIF' }, + + { word: 'STER', clue: 'Hemellichaam', startRow: 5, startCol: 0, direction: 'horizontal', answer: 'STER' }, + { word: 'EEND', clue: 'Watervogel', startRow: 5, startCol: 5, direction: 'horizontal', answer: 'EEND' }, + + { word: 'MAAN', clue: 'Nachthemel', startRow: 6, startCol: 0, direction: 'horizontal', answer: 'MAAN' }, + { word: 'REUS', clue: 'Gigant', startRow: 6, startCol: 5, direction: 'horizontal', answer: 'REUS' }, + + { word: 'REGEN', clue: 'Valt uit de lucht', startRow: 7, startCol: 0, direction: 'horizontal', answer: 'REGEN' }, + { word: 'VIS', clue: 'Zwemt in water', startRow: 7, startCol: 6, direction: 'horizontal', answer: 'VIS' }, + + // verticaal (spine) + { word: 'DONDER', clue: 'Weerverschijnsel', startRow: 1, startCol: 5, direction: 'vertical', answer: 'DONDER' } + ], + difficulty: 3, + rewards: { coins: 125, stars: 4, hints: 1 } +}; + +// Dutch Swedish-style Crossword Puzzles Database +const DUTCH_PUZZLES = { + // Level 1 - Easy + level1, + level0: { + 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' + }, + { + word: 'AT', + clue: 'Kleine woord', + startRow: 0, + startCol: 2, + direction: 'vertical', + answer: 'AT' + } + ], + difficulty: 1, + rewards: { coins: 50, stars: 3, hints: 1 } + }, + + // Level 2 - Medium Easy + level2: { + grid: [ + ['B', 'O', 'M', 'E', 'N'], + ['#', '#', '#', '#', 'D'], + ['H', 'O', 'N', 'D', 'E'], + ['#', '#', '#', '#', 'R'] + ], + 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' + }, + { + word: 'NE', + clue: 'Nee afkorting', + startRow: 0, + startCol: 4, + direction: 'vertical', + answer: 'NE' + } + ], + difficulty: 2, + rewards: { coins: 75, stars: 3, hints: 1 } + }, + + // Level 3 - Weather Theme + level3: { + grid: [ + ['R', 'E', 'G', 'E', 'N'], + ['#', '#', '#', '#', 'B'], + ['S', 'N', 'E', 'E', 'U'], + ['#', '#', '#', '#', 'W'] + ], + 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' + }, + { + word: 'NB', + clue: 'Nota bene', + startRow: 0, + startCol: 4, + direction: 'vertical', + answer: 'NB' + } + ], + difficulty: 3, + rewards: { coins: 100, stars: 3, hints: 1 } + }, + + // Daily Puzzle Template + daily: { + grid: [ + ['Z', 'O', 'N', 'N', 'E'], + ['#', '#', '#', '#', 'D'], + ['M', 'A', 'A', 'N', 'D'], + ['#', '#', '#', '#', 'G'] + ], + words: [ + { + word: 'ZONNE', + clue: 'Daglicht', + startRow: 0, + startCol: 0, + direction: 'horizontal', + answer: 'ZONNE' + }, + { + word: 'MAAND', + clue: 'Tijdseenheid', + startRow: 2, + startCol: 0, + direction: 'horizontal', + answer: 'MAAND' + }, + { + word: 'ND', + clue: 'Noord', + startRow: 0, + startCol: 4, + direction: 'vertical', + answer: 'ND' + } + ], + difficulty: 2, + rewards: { coins: 150, stars: 5, hints: 2 }, + dailyBonus: true + } +}; + +// Word Database for hints and validation +const DUTCH_WORD_DATABASE = { + animals: [ + { word: 'HAAS', clue: 'Snel dier' }, + { word: 'HOND', clue: 'Huisdier' }, + { word: 'KAT', clue: 'Katachtige' }, + { word: 'VLIEG', clue: 'Insect' }, + { word: 'VIS', clue: 'Zwemt in water' }, + { word: 'MUIS', clue: 'Klein dier' }, + { word: 'PAARD', clue: 'Rijdier' }, + { word: 'KIP', clue: 'Legt eieren' }, + { word: 'EEL', clue: 'Lang dier' } + ], + nature: [ + { word: 'BOMEN', clue: 'Planten' }, + { word: 'REGEN', clue: 'Valt uit de lucht' }, + { word: 'SNEEU', clue: 'Witte vlokken' }, + { word: 'DONDER', clue: 'Weer verschijnsel' }, + { word: 'BLIKSEM', clue: 'Lichtflits' }, + { word: 'MAAND', clue: 'Satelliet' }, + { word: 'STER', clue: 'Hemellichaam' }, + { word: 'WOLK', clue: 'Watten in lucht' }, + { word: 'WIND', clue: 'Luchtbeweging' }, + { word: 'ZON', clue: 'Ster' } + ], + common: [ + { word: 'AT', clue: 'Kleine woord' }, + { word: 'NE', clue: 'Nee afkorting' }, + { word: 'NB', clue: 'Nota bene' }, + { word: 'ND', clue: 'Noord' }, + { word: 'EN', clue: 'En' }, + { word: 'ER', clue: 'Er' }, + { word: 'IN', clue: 'In' }, + { word: 'OP', clue: 'Op' } + ] +}; + +// Export for use in game logic +if (typeof module !== 'undefined' && module.exports) { + module.exports = { DUTCH_PUZZLES, DUTCH_WORD_DATABASE }; +} diff --git a/public/redirect2.js b/public/redirect2.js deleted file mode 100644 index ae50a1c..0000000 --- a/public/redirect2.js +++ /dev/null @@ -1,43 +0,0 @@ -// Device Detection and Redirect -(function() { - // Check current page - const currentPage = window.location.pathname.split('/').pop() || 'index.html' - - // Check if we've already redirected (prevent infinite loops) - const hasRedirected = sessionStorage.getItem('hasRedirected') - if (hasRedirected) { - return - } - - // Detect device type - const width = window.innerWidth - const height = window.innerHeight - const userAgent = navigator.userAgent - - // Better mobile detection - const isMobilePhone = /iPhone|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) - const isTabletDevice = /iPad|Android(?!.*Mobile)/i.test(userAgent) - - let targetPage = null - - // Determine target page based on device and screen size - if (width < 400) { - // Mobile phones: detected mobile device < 768px - targetPage = 'mobile.html' - } else if (width < 400) { - // Small screens: force mobile - targetPage = 'mobile.html' - } else if (isTabletDevice || (width >= 500 && width < 1200)) { - // Tablets: iPad/Android tablet OR 500-1200px - use full app - targetPage = 'tablet-app.html' - } else { - // Desktop: >= 1200px - targetPage = 'index.html' - } - - // Redirect if needed - if (currentPage !== targetPage) { - sessionStorage.setItem('hasRedirected', 'true') - window.location.href = targetPage - } -})() diff --git a/tablet/404.html b/tablet/404.html new file mode 100644 index 0000000..260cc4c --- /dev/null +++ b/tablet/404.html @@ -0,0 +1,62 @@ + + + + + + Page Not Found + + + + + +

Page Not Found

+

Sorry, but the page you were trying to view does not exist.

+ + + + diff --git a/tablet/favicon.ico b/tablet/favicon.ico new file mode 100644 index 0000000..be74abd Binary files /dev/null and b/tablet/favicon.ico differ diff --git a/tablet/game.js b/tablet/game.js new file mode 100644 index 0000000..817bcd6 --- /dev/null +++ b/tablet/game.js @@ -0,0 +1,954 @@ +// 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() +}) diff --git a/tablet/icon.png b/tablet/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/tablet/icon.png differ diff --git a/tablet/icon.svg b/tablet/icon.svg new file mode 100644 index 0000000..f232922 --- /dev/null +++ b/tablet/icon.svg @@ -0,0 +1 @@ + diff --git a/public/tablet.html b/tablet/index.html similarity index 98% rename from public/tablet.html rename to tablet/index.html index 90668f6..29601b4 100644 --- a/public/tablet.html +++ b/tablet/index.html @@ -4,7 +4,6 @@ Kruiswoord Pro - diff --git a/tablet/puzzleData.js b/tablet/puzzleData.js new file mode 100644 index 0000000..d6a8e93 --- /dev/null +++ b/tablet/puzzleData.js @@ -0,0 +1,244 @@ +// Level 4 - 9x8 (Nature & Daily Words) +const level1 = { + grid: [ + ['B','O','M','E','N','#','Z','O','N'], + ['H','A','A','S','#','D','O','R','P'], + ['H','O','N','D','#','O','P','E','N'], + ['W','I','N','D','#','N','E','S','T'], + ['W','O','L','K','#','D','U','I','F'], + ['S','T','E','R','#','E','E','N','D'], + ['M','A','A','N','#','R','E','U','S'], + ['R','E','G','E','N','#','V','I','S'] + ], + words: [ + // horizontaal + { word: 'BOMEN', clue: 'Planten', startRow: 0, startCol: 0, direction: 'horizontal', answer: 'BOMEN' }, + { word: 'ZON', clue: 'Ster aan de hemel', startRow: 0, startCol: 6, direction: 'horizontal', answer: 'ZON' }, + + { word: 'HAAS', clue: 'Snel dier', startRow: 1, startCol: 0, direction: 'horizontal', answer: 'HAAS' }, + { word: 'DORP', clue: 'Kleine plaats', startRow: 1, startCol: 5, direction: 'horizontal', answer: 'DORP' }, + + { word: 'HOND', clue: 'Huisdier', startRow: 2, startCol: 0, direction: 'horizontal', answer: 'HOND' }, + { word: 'OPEN', clue: 'Niet dicht', startRow: 2, startCol: 5, direction: 'horizontal', answer: 'OPEN' }, + + { word: 'WIND', clue: 'Luchtbeweging', startRow: 3, startCol: 0, direction: 'horizontal', answer: 'WIND' }, + { word: 'NEST', clue: 'Thuis van een vogel', startRow: 3, startCol: 5, direction: 'horizontal', answer: 'NEST' }, + + { word: 'WOLK', clue: 'In de lucht', startRow: 4, startCol: 0, direction: 'horizontal', answer: 'WOLK' }, + { word: 'DUIF', clue: 'Stadsvogel', startRow: 4, startCol: 5, direction: 'horizontal', answer: 'DUIF' }, + + { word: 'STER', clue: 'Hemellichaam', startRow: 5, startCol: 0, direction: 'horizontal', answer: 'STER' }, + { word: 'EEND', clue: 'Watervogel', startRow: 5, startCol: 5, direction: 'horizontal', answer: 'EEND' }, + + { word: 'MAAN', clue: 'Nachthemel', startRow: 6, startCol: 0, direction: 'horizontal', answer: 'MAAN' }, + { word: 'REUS', clue: 'Gigant', startRow: 6, startCol: 5, direction: 'horizontal', answer: 'REUS' }, + + { word: 'REGEN', clue: 'Valt uit de lucht', startRow: 7, startCol: 0, direction: 'horizontal', answer: 'REGEN' }, + { word: 'VIS', clue: 'Zwemt in water', startRow: 7, startCol: 6, direction: 'horizontal', answer: 'VIS' }, + + // verticaal (spine) + { word: 'DONDER', clue: 'Weerverschijnsel', startRow: 1, startCol: 5, direction: 'vertical', answer: 'DONDER' } + ], + difficulty: 3, + rewards: { coins: 125, stars: 4, hints: 1 } +}; + +// Dutch Swedish-style Crossword Puzzles Database +const DUTCH_PUZZLES = { + // Level 1 - Easy + level1, + level0: { + 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' + }, + { + word: 'AT', + clue: 'Kleine woord', + startRow: 0, + startCol: 2, + direction: 'vertical', + answer: 'AT' + } + ], + difficulty: 1, + rewards: { coins: 50, stars: 3, hints: 1 } + }, + + // Level 2 - Medium Easy + level2: { + grid: [ + ['B', 'O', 'M', 'E', 'N'], + ['#', '#', '#', '#', 'D'], + ['H', 'O', 'N', 'D', 'E'], + ['#', '#', '#', '#', 'R'] + ], + 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' + }, + { + word: 'NE', + clue: 'Nee afkorting', + startRow: 0, + startCol: 4, + direction: 'vertical', + answer: 'NE' + } + ], + difficulty: 2, + rewards: { coins: 75, stars: 3, hints: 1 } + }, + + // Level 3 - Weather Theme + level3: { + grid: [ + ['R', 'E', 'G', 'E', 'N'], + ['#', '#', '#', '#', 'B'], + ['S', 'N', 'E', 'E', 'U'], + ['#', '#', '#', '#', 'W'] + ], + 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' + }, + { + word: 'NB', + clue: 'Nota bene', + startRow: 0, + startCol: 4, + direction: 'vertical', + answer: 'NB' + } + ], + difficulty: 3, + rewards: { coins: 100, stars: 3, hints: 1 } + }, + + // Daily Puzzle Template + daily: { + grid: [ + ['Z', 'O', 'N', 'N', 'E'], + ['#', '#', '#', '#', 'D'], + ['M', 'A', 'A', 'N', 'D'], + ['#', '#', '#', '#', 'G'] + ], + words: [ + { + word: 'ZONNE', + clue: 'Daglicht', + startRow: 0, + startCol: 0, + direction: 'horizontal', + answer: 'ZONNE' + }, + { + word: 'MAAND', + clue: 'Tijdseenheid', + startRow: 2, + startCol: 0, + direction: 'horizontal', + answer: 'MAAND' + }, + { + word: 'ND', + clue: 'Noord', + startRow: 0, + startCol: 4, + direction: 'vertical', + answer: 'ND' + } + ], + difficulty: 2, + rewards: { coins: 150, stars: 5, hints: 2 }, + dailyBonus: true + } +}; + +// Word Database for hints and validation +const DUTCH_WORD_DATABASE = { + animals: [ + { word: 'HAAS', clue: 'Snel dier' }, + { word: 'HOND', clue: 'Huisdier' }, + { word: 'KAT', clue: 'Katachtige' }, + { word: 'VLIEG', clue: 'Insect' }, + { word: 'VIS', clue: 'Zwemt in water' }, + { word: 'MUIS', clue: 'Klein dier' }, + { word: 'PAARD', clue: 'Rijdier' }, + { word: 'KIP', clue: 'Legt eieren' }, + { word: 'EEL', clue: 'Lang dier' } + ], + nature: [ + { word: 'BOMEN', clue: 'Planten' }, + { word: 'REGEN', clue: 'Valt uit de lucht' }, + { word: 'SNEEU', clue: 'Witte vlokken' }, + { word: 'DONDER', clue: 'Weer verschijnsel' }, + { word: 'BLIKSEM', clue: 'Lichtflits' }, + { word: 'MAAND', clue: 'Satelliet' }, + { word: 'STER', clue: 'Hemellichaam' }, + { word: 'WOLK', clue: 'Watten in lucht' }, + { word: 'WIND', clue: 'Luchtbeweging' }, + { word: 'ZON', clue: 'Ster' } + ], + common: [ + { word: 'AT', clue: 'Kleine woord' }, + { word: 'NE', clue: 'Nee afkorting' }, + { word: 'NB', clue: 'Nota bene' }, + { word: 'ND', clue: 'Noord' }, + { word: 'EN', clue: 'En' }, + { word: 'ER', clue: 'Er' }, + { word: 'IN', clue: 'In' }, + { word: 'OP', clue: 'Op' } + ] +}; + +// Export for use in game logic +if (typeof module !== 'undefined' && module.exports) { + module.exports = { DUTCH_PUZZLES, DUTCH_WORD_DATABASE }; +} diff --git a/public/tablet-app.css b/tablet/tablet-app.css similarity index 100% rename from public/tablet-app.css rename to tablet/tablet-app.css diff --git a/public/tablet-app.html b/tablet/tablet-app.html similarity index 100% rename from public/tablet-app.html rename to tablet/tablet-app.html diff --git a/public/tablet-app.js b/tablet/tablet-app.js similarity index 100% rename from public/tablet-app.js rename to tablet/tablet-app.js diff --git a/public/tablet.css b/tablet/tablet.css similarity index 100% rename from public/tablet.css rename to tablet/tablet.css diff --git a/public/tablet.js b/tablet/tablet.js similarity index 100% rename from public/tablet.js rename to tablet/tablet.js