Ga direct naar de hoofdinhoud
Klasklaar.nl
Home
Blog
Gr. 1/2
Inloopactiviteiten
Thema's
Thema Herfst
Thema Ruimte
Thema Sinterklaas
Thema Boerderij
Thema Vlinder
Thema Vervoer
Thema Bouwen
Thema Koningsspelen
Thema Gezondheid
Motoriek 1/2
Rekenen 1/2
Lezen 1/2
Verklanken
Voorleesteksten
SEO 1/2
Energizers
dagritmekaarten
Gr. 3
Thema's
Rekenen 3
Bussommen
Dobbelsteen sommen
Contextsommen
Lezen 3
Teksten op thema
Sinterklaas
Kerst
Carnaval
Feest
Koningsspelen
Beroepen
De bakker
De bouw
De hulpdiensten
De post
De bibliotheek
De boerderij
De speeltuin
De tuin
Dierentuin
Het bos
het restaurant
Strand
Het zwembad
Thuis
School
Supermarkt
Verkeer
Het vliegtuig
De trein
Winter
Lente
Zomer
Herfst
Piraten
Sprookjes
Familie
Huisdieren
Kleding
Gezondheid
Gezond eten
Sport
Insecten
Teksten B3
Teksten M3
Teksten E3
Lees en kleur
Luister, lees en kies
Lees en zoek
Waar of niet waar
SEO 3
Creatief
dagritmekaarten
Gr. 4
Rekenen 4
Getallen en bewerkingen
Verhaaltjessommen
Werkbladen
Rekenspelletjes
Meten
Verhaaltjessommen
Werkbladen
Tijd
Verhaaltjessommen
Werkbladen
Geld
Verhaaltjessommen
Werkbladen
Som bij het plaatje
Steunkaarten
Taal 4
Woordbenoemen
Zinsontleding
Taal Algemeen
Steunkaarten
Spelling 4
Steunkaarten
Spellingwerkbladen
Spellen
Lezen 4
Teksten B4
Teksten M4
Teksten E4
Leeslessen 4
Begrijpend Lezen 4
SEO 4
Overig
Geschiedenis 4
dagritmekaarten
Gr. 5
Rekenen 5
Getallen en bewerkingen
Verhaalsommen
Werkbladen
Rekenspelletjes
Meten
Werkbladen
Verhaalsommen
Tijd
Verhaalsommen
Werkbladen
Geld
Verhaalsommen
Werkbladen
Breuken
Verhaalsommen
Werkbladen
Steunkaarten
Taal 5
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 5
Spellingwerkbladen
Spellen
Lezen 5
Teksten B5
Teksten M5
Teksten E5
Leeslessen 5
Begrijpend Lezen 5
SEO 5
Geschiedenis 5
Gr. 6
Rekenen 6
Getallen en Bewerkingen
Verhaalsommen
Werkbladen
Meten
Verhaalsommen
Werkbladen
Tijd
Verhaalsommen
Werkbladen
Geld
Verhaalsommen
Werkbladen
Breuken
Verhaalsommen
Werkbladen
Procenten
Verhaalsommen
Werkbladen
Steunkaarten
Taal 6
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 6
Spellingwerkbladen
Spellen
Werkwoordspelling
Lezen 6
Teksten B6
Teksten M6
Teksten E6
Leeslessen 6
Begrijpend Lezen 6
SEO 6
Gr. 7
Rekenen 7
Getallen en Bewerkingen
Werkbladen
Verhaalsommen
Meten
Werkbladen
Verhaalsommen
Tijd
Werkbladen
Verhaalsommen
Geld
Werkbladen
Verhaalsommen
Breuken
Verhaalsommen
Werkbladen
Procenten
Werkbladen
Verhaalsommen
Verhoudingen
Steunkaarten
Taal 7
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 7
Spellingwerkbladen
Spellen
Lezen 7
Teksten B7
Teksten M7
Teksten E7
Leeslessen 7
Begrijpend Lezen 7
SEO 7
Gr. 8
Rekenen 8
Getallen en bewerkingen
Verhaalsommen
Werkbladen
Meten
Verhaalsommen
Werkbladen
Tijd
Verhaalsommen
Werkbladen
Geld
Verhaalsommen
Werkbladen
Breuken
Verhaalsommen
Werkbladen
Procenten
Verhaalsommen
Werkbladen
Verhoudingen
Steunkaarten
Taal 8
Woordbenoemen
Zinsontleding
Taal Algemeen
Spelling 8
Spellingwerkbladen
Spellen
Lezen 8
Leeslessen
Lezen B8
Begrijpend Lezen
SEO 8
Afscheid
Didactiek
Sommenmaker
Taalmaker
Woordbenoemen generator
Woorden generator
Zinnen generator
NT2
Lezen
Pictokaarten
Taal
Portfolio
Dit ben ik
Mijn werk in beeld
Doeboekjes
Prentenboeken
Vlam en het feest
Blaadje en de storm
Kooltje en steenkolen
Wolkje in de lucht
Tok en Tik - De taart
Tok en Tik - We gaan je missen
Tok en Tik - De storm
Tok en Tik - Het ruim
Groepsvorming
Gouden Weken
Kinderliedjes
Digitale Geletterdheid
B-bot
Techniek
Kleurplaten
Sinterklaas
Kerst
Pasen
Carnaval
Koningsdag
Koningsspelen
Oud en Nieuw
Verjaardag
Moederdag
Vaderdag
Halloween
Valentijn
Lente
Zomer
Herfst
Winter
Regen
Sneeuw
Strand
Bloemen
Paddenstoelen
Weer
Bakker
Bouw
Kunstenaar
Politie
Brandweer
Dokter
Tandarts
Boer
Kapper
Post
Winkel
Auto
Tractor
Bus
Vliegtuig
Boot
Fiets
Trein
Vrachtwagen
Graafmachine
Brandweerauto
Politieauto
Voetbal
Hockey
Korfbal
Zwemmen
Dansen
Turnen
Sportdag
Speeltuin
Speelgoed
Spelletjes
Sprookjes
Draken
Ridders
Piraten
Prinsessen
Zeemeerminnen
Heksen
Tovenaars
Eenhoorns
Monsters
Natuur
Bos
Boerderij
Ruimte
Verkeer
Gezondheid
Lichaam
Eten en drinken
Techniek
Recycling
Boerderijdieren
Dierentuin
Huisdieren
Bosdieren
Zeedieren
Insecten
Vogels
Dino's
Paarden
Honden
Katten
Ballet
Timer
Sommenflitser
Woordenflitser
Keuzerad
Dagstarter
Praatplaten
Naamplaatjes
Splitsmachine
Bussommen
Rekenrek
AR Aquarium
Woordzoeker maker
Online spellen
Blikgooien (tafels)
Eendjes vangen (tafels)
Ringwerpen (tafels)
Roodkapje (splitsen)
Hans en Grietje (plus en min)
Zeven kleine geitjes (plus via de 10)
Verliefde harten
Ruimtemissie (automatiseren)
Locomotief (automatiseren)
contact
Zoeken
Woordzoeker maken
Maak een printklare woordzoeker. Klasklaar regelt het rooster en de opmaak.
Werkblad
Antwoordblad
Nog geen puzzel
Woordzoeker wordt gemaakt
De preview verschijnt hier automatisch.
(function(){ 'use strict'; try{ /* * Klasklaar Word Search Engine * Pure puzzle logic: no DOM dependencies. */ const WS = (() => { const MIN_GRID_SIZE = 6; const MAX_GRID_SIZE = 25; const DEFAULT_MAX_NODES = 120000; const DEFAULT_ATTEMPTS = 18; const DIRECTIONS = Object.freeze({ R: { key:'R', dr:0, dc:1, family:'H', label:'→' }, L: { key:'L', dr:0, dc:-1, family:'H', label:'←' }, D: { key:'D', dr:1, dc:0, family:'V', label:'↓' }, U: { key:'U', dr:-1, dc:0, family:'V', label:'↑' }, DR: { key:'DR', dr:1, dc:1, family:'D1', label:'↘' }, UL: { key:'UL', dr:-1, dc:-1, family:'D1', label:'↖' }, DL: { key:'DL', dr:1, dc:-1, family:'D2', label:'↙' }, UR: { key:'UR', dr:-1, dc:1, family:'D2', label:'↗' } }); const LEVELS = Object.freeze({ easy: ['R','D'], normal: ['R','D','DR','DL'], hard: ['R','L','D','U','DR','UL','DL','UR'] }); const ERROR = Object.freeze({ NO_WORDS: 'NO_WORDS', DUPLICATE_WORD: 'DUPLICATE_WORD', SUBSTRING_WORD: 'SUBSTRING_WORD', INVALID_CHARACTERS: 'INVALID_CHARACTERS', WORD_TOO_LONG: 'WORD_TOO_LONG', ANSWER_TOO_LONG: 'ANSWER_TOO_LONG', GRID_TOO_SMALL: 'GRID_TOO_SMALL', ANSWER_GRID_TOO_LARGE: 'ANSWER_GRID_TOO_LARGE', ANSWER_TOO_MANY_CROSSINGS: 'ANSWER_TOO_MANY_CROSSINGS', SEARCH_EXHAUSTED: 'SEARCH_EXHAUSTED', FINAL_VALIDATION_FAILED: 'FINAL_VALIDATION_FAILED' }); function normalizeSpaces(s) { return String(s ?? '').normalize('NFC').trim().replace(/\s+/g, ' '); } function normalizeGridText(s) { const display = normalizeSpaces(s); // Keep letters incl. diacritics and digits; remove spaces and common separators. const grid = display.toLocaleUpperCase('nl-NL') .replace(/[\s\-‐‑‒–—_'.’]/g, '') .replace(/[^\p{L}\p{N}]/gu, ''); return { display, grid }; } function graphemes(s) { return Array.from(String(s)); } function reverseString(s) { return graphemes(s).reverse().join(''); } function normalizeWord(raw, id) { const { display, grid } = normalizeGridText(raw); return { id, raw: String(raw ?? ''), displayText: display, gridText: grid, letters: graphemes(grid), length: graphemes(grid).length }; } function normalizeAnswer(raw) { const display = normalizeSpaces(raw); const grid = display.toLocaleUpperCase('nl-NL') .replace(/[^\p{L}\p{N}]/gu, ''); return { displayText: display, gridText: grid, letters: graphemes(grid), length: graphemes(grid).length, groups: display ? display.split(/\s+/).map(part => graphemes(part.replace(/[^\p{L}\p{N}]/gu,'')).length).filter(Boolean) : [] }; } function normalizeWords(rawWords) { const arr = Array.isArray(rawWords) ? rawWords : String(rawWords ?? '').split(/\r?\n/); const out = []; for (const raw of arr) { const w = normalizeWord(raw, out.length); if (w.gridText) out.push(w); } return out; } function validateInput(rawWords, options={}) { const words = normalizeWords(rawWords); const answer = normalizeAnswer(options.answer || ''); const size = Number(options.size || 12); const errors = []; const warnings = []; if (!words.length) errors.push({ code:ERROR.NO_WORDS }); const seen = new Map(); for (const w of words) { if (!/^[\p{L}\p{N}]+$/u.test(w.gridText)) { errors.push({ code:ERROR.INVALID_CHARACTERS, word:w.displayText }); } if (seen.has(w.gridText)) { errors.push({ code:ERROR.DUPLICATE_WORD, word:w.displayText, other:seen.get(w.gridText).displayText }); } else seen.set(w.gridText, w); } // Substring conflict: a shorter target is already physically present inside a longer target. for (let i=0;i<words.length;i++) { for (let j=i+1;j<words.length;j++) { const a = words[i], b = words[j]; const [shorter,longer] = a.length <= b.length ? [a,b] : [b,a]; if (shorter.gridText && (longer.gridText.includes(shorter.gridText) || longer.gridText.includes(reverseString(shorter.gridText)))) { errors.push({ code:ERROR.SUBSTRING_WORD, word:shorter.displayText, other:longer.displayText }); } } } if (Number.isFinite(size)) { for (const w of words) { if (w.length > size) errors.push({ code:ERROR.WORD_TOO_LONG, word:w.displayText, length:w.length, size }); } } return { valid:errors.length===0, errors, warnings, words, answer }; } function createRng(seed) { let a = (Number(seed) >>> 0) || 0x12345678; return function rng() { // mulberry32 a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } function hashSeed(input) { let h = 2166136261 >>> 0; for (const ch of String(input)) { h ^= ch.codePointAt(0); h = Math.imul(h, 16777619); } return h >>> 0; } function indexOfCell(row,col,size) { return row*size + col; } function rowOf(index,size) { return Math.floor(index/size); } function colOf(index,size) { return index % size; } function createBoard(size) { const n = size*size; return { size, chars: new Array(n).fill(null), owner1: new Int32Array(n).fill(-1), owner2: new Int32Array(n).fill(-1), placed: new Map(), occupied: 0, crossings: 0, directionCounts: {H:0,V:0,D1:0,D2:0}, rowUsage: new Int16Array(size), colUsage: new Int16Array(size), }; } function placementCells(word, startRow, startCol, dir, size) { const cells = []; for (let i=0;i<word.length;i++) { const r = startRow + dir.dr*i; const c = startCol + dir.dc*i; if (r<0 || r>=size || c<0 || c>=size) return null; cells.push(indexOfCell(r,c,size)); } return cells; } function createPlacements(word, size, directionKeys) { const out = []; for (const key of directionKeys) { const dir = DIRECTIONS[key]; for (let r=0;r<size;r++) { for (let c=0;c<size;c++) { const cells = placementCells(word,r,c,dir,size); if (!cells) continue; out.push({ wordId: word.id, startRow:r, startCol:c, direction:key, family:dir.family, cells }); } } } return out; } function sharedLetters(a,b) { const set = new Set(a.letters); const common = new Set(); for (const ch of b.letters) if (set.has(ch)) common.add(ch); return common; } function crossingCompatibility(words) { const matrix = Array.from({length:words.length},()=>new Uint8Array(words.length)); let compatiblePairs = 0; for (let i=0;i<words.length;i++) { for (let j=i+1;j<words.length;j++) { if (sharedLetters(words[i],words[j]).size>0) { matrix[i][j]=matrix[j][i]=1; compatiblePairs++; } } } return { matrix, compatiblePairs }; } function answerFeasibility(words, answer, size) { const G = size*size; const T = words.reduce((s,w)=>s+w.length,0); const L = answer.length; const C = T + L - G; const longest = words.reduce((m,w)=>Math.max(m,w.length),0); if (longest > size) return { status:'IMPOSSIBLE', code:ERROR.GRID_TOO_SMALL, longest, size, G,T,L,C }; if (C < 0) return { status:'IMPOSSIBLE', code:ERROR.ANSWER_GRID_TOO_LARGE, G,T,L,C }; const { compatiblePairs } = crossingCompatibility(words); const maxCrossByLetters = Math.floor(T/2); const maxCrossings = Math.min(compatiblePairs, maxCrossByLetters); if (C > maxCrossings) return { status:'IMPOSSIBLE', code:ERROR.ANSWER_TOO_MANY_CROSSINGS, G,T,L,C,maxCrossings,compatiblePairs }; return { status:'POSSIBLE', G,T,L,C,maxCrossings,compatiblePairs, targetOccupied:G-L }; } function normalFeasibility(words, size) { const longest = words.reduce((m,w)=>Math.max(m,w.length),0); if (longest > size) return { status:'IMPOSSIBLE', code:ERROR.GRID_TOO_SMALL, longest, size }; return { status:'POSSIBLE' }; } function checkPlacement(board, placement, word, targetCrossings=null, targetOccupied=null) { const touchedOwners = new Set(); let newCrossings = 0; let newCells = 0; for (let i=0;i<placement.cells.length;i++) { const idx = placement.cells[i]; const ch = word.letters[i]; const existing = board.chars[idx]; if (existing === null) { newCells++; continue; } if (existing !== ch) return { allowed:false, reason:'LETTER_CONFLICT' }; if (board.owner2[idx] !== -1) return { allowed:false, reason:'THIRD_WORD_IN_CELL' }; const owner = board.owner1[idx]; if (owner === -1) return { allowed:false, reason:'CORRUPT_BOARD' }; const existingPlacement = board.placed.get(owner); if (!existingPlacement) return { allowed:false, reason:'CORRUPT_BOARD' }; if (existingPlacement.family === placement.family) return { allowed:false, reason:'PARALLEL_OVERLAP' }; if (touchedOwners.has(owner)) return { allowed:false, reason:'MULTI_CELL_OVERLAP' }; touchedOwners.add(owner); newCrossings++; } if (targetCrossings !== null && board.crossings + newCrossings > targetCrossings) { return { allowed:false, reason:'TOO_MANY_CROSSINGS' }; } if (targetOccupied !== null && board.occupied + newCells > targetOccupied) { return { allowed:false, reason:'TOO_MANY_OCCUPIED' }; } return { allowed:true, newCrossings, newCells, touchedOwners }; } function applyPlacement(board, placement, word, check) { const undo = { wordId:word.id, placement, oldCrossings:board.crossings, oldOccupied:board.occupied, changed:[], oldDirectionCount: board.directionCounts[placement.family] }; for (let i=0;i<placement.cells.length;i++) { const idx = placement.cells[i]; undo.changed.push({ idx, char:board.chars[idx], owner1:board.owner1[idx], owner2:board.owner2[idx] }); if (board.chars[idx] === null) { board.chars[idx] = word.letters[i]; board.owner1[idx] = word.id; board.occupied++; board.rowUsage[rowOf(idx,board.size)]++; board.colUsage[colOf(idx,board.size)]++; } else { board.owner2[idx] = word.id; board.crossings++; } } board.placed.set(word.id, placement); board.directionCounts[placement.family]++; return undo; } function undoPlacement(board, undo) { board.placed.delete(undo.wordId); for (const change of undo.changed) { const idx = change.idx; const currentWasOwnedOnce = change.char === null; if (currentWasOwnedOnce) { board.rowUsage[rowOf(idx,board.size)]--; board.colUsage[colOf(idx,board.size)]--; } board.chars[idx] = change.char; board.owner1[idx] = change.owner1; board.owner2[idx] = change.owner2; } board.crossings = undo.oldCrossings; board.occupied = undo.oldOccupied; board.directionCounts[undo.placement.family] = undo.oldDirectionCount; } function scorePlacement(board, placement, check, rng, answerMode=false, targetCrossings=0) { const size = board.size; let score = 0; // Prefer under-used direction families. score -= board.directionCounts[placement.family] * 5; // Prefer cells in less used rows/columns. let usagePenalty = 0; let edgeTouches = 0; let centerDist = 0; const center = (size-1)/2; for (const idx of placement.cells) { const r=rowOf(idx,size), c=colOf(idx,size); usagePenalty += board.rowUsage[r] + board.colUsage[c]; if (r===0||c===0||r===size-1||c===size-1) edgeTouches++; centerDist += Math.abs(r-center)+Math.abs(c-center); } score -= usagePenalty * 0.55; score += edgeTouches * 0.7; // some edge use helps spread score += centerDist * 0.02; if (answerMode) { const remainingNeed = targetCrossings - board.crossings; // Reward crossings when we still need them, otherwise neutral. score += Math.min(check.newCrossings, remainingNeed) * 10; } else { score += check.newCrossings * 2.2; } score += rng()*3; return score; } function remainingCrossingUpperBound(board, remainingWords, compatibilityMatrix) { // Safe, deliberately loose upper bound. Compatibility is precomputed once. let upper=0; const placedIds = Array.from(board.placed.keys()); for (const w of remainingWords) { for (const pid of placedIds) if (compatibilityMatrix[w.id][pid]) upper++; } for (let i=0;i<remainingWords.length;i++) { for (let j=i+1;j<remainingWords.length;j++) { if (compatibilityMatrix[remainingWords[i].id][remainingWords[j].id]) upper++; } } return upper; } function chooseNextWord(board, remainingWords, placementsByWord, constraints, rng) { // Full dynamic MRV is expensive for large puzzles. Inspect the few statically hardest // remaining words (long words / few geometric placements) and do MRV inside that pool. const pool = remainingWords.slice().sort((a,b)=> placementsByWord[a.id].length - placementsByWord[b.id].length || b.length-a.length ).slice(0, Math.min(4, remainingWords.length)); let best = null; for (const w of pool) { const legal = []; for (const p of placementsByWord[w.id]) { const check = checkPlacement(board,p,w,constraints.targetCrossings,constraints.targetOccupied); if (check.allowed) legal.push({p,check}); } if (legal.length===0) return {word:w, legal:[]}; const difficulty = legal.length * 1000 - w.length*10 + rng(); if (!best || difficulty < best.difficulty) best={word:w,legal,difficulty}; } return best; } function solveAttempt(words, size, directionKeys, options={}) { const seed = options.seed ?? 1; const rng = createRng(seed); const answer = options.answer || normalizeAnswer(''); const answerMode = !!(answer && answer.length); const feasibility = answerMode ? answerFeasibility(words,answer,size) : normalFeasibility(words,size); if (feasibility.status==='IMPOSSIBLE') return {ok:false, type:'PROVEN_IMPOSSIBLE', error:feasibility}; const placementsByWord = {}; for (const w of words) placementsByWord[w.id] = createPlacements(w,size,directionKeys); const compatibilityMatrix = crossingCompatibility(words).matrix; const board = createBoard(size); const maxNodes = options.maxNodes || DEFAULT_MAX_NODES; const targetCrossings = answerMode ? feasibility.C : null; const targetOccupied = answerMode ? feasibility.targetOccupied : null; let nodes = 0; let backtracks = 0; function recurse(remainingWords) { nodes++; if (nodes > maxNodes) return false; if (remainingWords.length===0) { if (answerMode) { return board.crossings===targetCrossings && board.occupied===targetOccupied; } return true; } if (answerMode) { if (board.crossings > targetCrossings || board.occupied > targetOccupied) return false; const remainingLetters = remainingWords.reduce((s,w)=>s+w.length,0); // Maximum unique occupancy if every remaining letter goes into an empty cell. if (board.occupied + remainingLetters < targetOccupied) return false; // Minimum unique occupancy assuming each remaining word could cross each of its letters is too weak; crossing bound helps. const needCross = targetCrossings - board.crossings; if (needCross < 0) return false; const upperCross = remainingCrossingUpperBound(board,remainingWords,compatibilityMatrix); if (needCross > upperCross) return false; } const choice = chooseNextWord(board,remainingWords,placementsByWord,{targetCrossings,targetOccupied},rng); if (!choice || choice.legal.length===0) return false; const scored = choice.legal.map(item=>({ ...item, score:scorePlacement(board,item.p,item.check,rng,answerMode,targetCrossings||0) })).sort((a,b)=>b.score-a.score); const nextRemaining = remainingWords.filter(w=>w.id!==choice.word.id); for (const item of scored) { const undo = applyPlacement(board,item.p,choice.word,item.check); if (recurse(nextRemaining)) return true; undoPlacement(board,undo); backtracks++; } return false; } const initial = words.slice().sort((a,b)=>b.length-a.length); const ok = recurse(initial); if (!ok) return {ok:false,type:'SEARCH_EXHAUSTED',stats:{nodes,backtracks}, feasibility}; return { ok:true, board, stats:{nodes,backtracks}, feasibility, placements:Array.from(board.placed.entries()).sort((a,b)=>a[0]-b[0]).map(([wordId,p])=>({...p,wordId})) }; } // Dutch-ish letter frequencies. Approximate, enough for natural-looking filler. const LETTER_BAG = [ ['E',19],['N',10],['A',8],['T',7],['I',7],['R',6],['O',6],['D',6],['S',4],['L',4],['G',3],['V',3],['K',3],['M',2.5],['U',2.2],['B',1.7],['P',1.6],['H',1.5],['J',1.4],['Z',1.3],['W',1.2],['F',0.8],['C',0.7],['X',0.1],['Y',0.1],['Q',0.05] ]; const LETTER_TOTAL = LETTER_BAG.reduce((s,[,w])=>s+w,0); function randomDutchLetter(rng) { let x = rng()*LETTER_TOTAL; for (const [ch,w] of LETTER_BAG) { x -= w; if (x<=0) return ch; } return 'E'; } function allDirectionKeys() { return Object.keys(DIRECTIONS); } function canonicalOccurrence(cells) { const a = cells.join(','); const b = cells.slice().reverse().join(','); return a < b ? a : b; } function findOccurrences(chars,size,wordText) { const letters = graphemes(wordText); const found = new Map(); if (!letters.length) return []; for (let r=0;r<size;r++) { for (let c=0;c<size;c++) { for (const key of allDirectionKeys()) { const d = DIRECTIONS[key]; const cells=[]; let ok=true; for (let i=0;i<letters.length;i++) { const rr=r+d.dr*i, cc=c+d.dc*i; if (rr<0||rr>=size||cc<0||cc>=size) { ok=false; break; } const idx=indexOfCell(rr,cc,size); if (chars[idx]!==letters[i]) { ok=false; break; } cells.push(idx); } if (ok) found.set(canonicalOccurrence(cells),cells); } } } return Array.from(found.values()); } function fillRandomCells(chars,size,words,rng,maxPasses=40) { const empty=[]; for (let i=0;i<chars.length;i++) if (chars[i]===null) empty.push(i); for (const idx of empty) chars[idx]=randomDutchLetter(rng); // Repair accidental duplicates using only filler cells. const fillerSet = new Set(empty); for (let pass=0;pass<maxPasses;pass++) { let conflict=null; for (const w of words) { const occ = findOccurrences(chars,size,w.gridText); if (occ.length>1) { // Intended placement is unknown here; choose any occurrence containing filler as repair target. const repairable = occ.find(cells=>cells.some(i=>fillerSet.has(i))); if (repairable) { conflict={w,cells:repairable}; break; } return {ok:false, reason:'UNREPAIRABLE_EXTRA_OCCURRENCE'}; } } if (!conflict) return {ok:true}; const candidates=conflict.cells.filter(i=>fillerSet.has(i)); if (!candidates.length) return {ok:false,reason:'UNREPAIRABLE_EXTRA_OCCURRENCE'}; const idx=candidates[Math.floor(rng()*candidates.length)]; const old=chars[idx]; let changed=false; for (let tries=0;tries<12;tries++) { const ch=randomDutchLetter(rng); if (ch===old) continue; chars[idx]=ch; changed=true; break; } if (!changed) chars[idx]=old==='E'?'A':'E'; } return {ok:false,reason:'REPAIR_LIMIT'}; } function fillAnswerCells(chars, answer) { const empty=[]; for (let i=0;i<chars.length;i++) if (chars[i]===null) empty.push(i); if (empty.length!==answer.length) return {ok:false, reason:'ANSWER_EMPTY_COUNT_MISMATCH', empty}; for (let i=0;i<empty.length;i++) chars[empty[i]]=answer.letters[i]; return {ok:true, answerCells:empty}; } function samePhysicalOccurrence(a,b) { return canonicalOccurrence(a)===canonicalOccurrence(b); } function validateFinalPuzzle(result) { const {size,chars,words,placements,answer,answerCells} = result; const errors=[]; if (!Array.isArray(chars) || chars.length!==size*size || chars.some(ch=>!ch)) errors.push('GRID_NOT_FULL'); const placementById = new Map(placements.map(p=>[p.wordId,p])); // Reconstruct owners from placements, independently of solver board. const owners = Array.from({length:size*size},()=>[]); for (const w of words) { const p=placementById.get(w.id); if (!p) { errors.push(`MISSING_PLACEMENT:${w.id}`); continue; } const dir=DIRECTIONS[p.direction]; if (!dir) { errors.push(`BAD_DIRECTION:${w.id}`); continue; } if (p.cells.length!==w.length) { errors.push(`BAD_LENGTH:${w.id}`); continue; } for (let i=0;i<p.cells.length;i++) { const idx=p.cells[i]; if (chars[idx]!==w.letters[i]) errors.push(`PLACEMENT_MISMATCH:${w.id}`); owners[idx].push({wordId:w.id,family:p.family}); } } for (let i=0;i<owners.length;i++) { if (owners[i].length>2) errors.push(`TOO_MANY_OWNERS:${i}`); if (owners[i].length===2 && owners[i][0].family===owners[i][1].family) errors.push(`PARALLEL_OVERLAP:${i}`); } for (let i=0;i<words.length;i++) { for (let j=i+1;j<words.length;j++) { let shared=0; const pi=placementById.get(words[i].id), pj=placementById.get(words[j].id); if (!pi||!pj) continue; const set=new Set(pi.cells); for (const cell of pj.cells) if (set.has(cell)) shared++; if (shared>1) errors.push(`MULTI_CELL_OVERLAP:${words[i].id}:${words[j].id}`); } } // Every search word should have exactly one physical occurrence in whole grid. for (const w of words) { const occ=findOccurrences(chars,size,w.gridText); if (occ.length!==1) errors.push(`OCCURRENCE_COUNT:${w.id}:${occ.length}`); else { const p=placementById.get(w.id); if (p && !samePhysicalOccurrence(occ[0],p.cells)) errors.push(`WRONG_OCCURRENCE:${w.id}`); } } if (answer && answer.length) { const unowned=[]; for (let i=0;i<owners.length;i++) if (owners[i].length===0) unowned.push(i); if (unowned.length!==answer.length) errors.push(`ANSWER_COUNT:${unowned.length}:${answer.length}`); const letters=unowned.map(i=>chars[i]).join(''); if (letters!==answer.gridText) errors.push(`ANSWER_TEXT:${letters}:${answer.gridText}`); if (answerCells && answerCells.join(',')!==unowned.join(',')) errors.push('ANSWER_CELLS_ORDER'); } return {valid:errors.length===0,errors}; } function qualityScore(result) { const {size,placements,board,answerCells=[]}=result; let score=100; const fam={H:0,V:0,D1:0,D2:0}; for (const p of placements) fam[p.family]++; const counts=Object.values(fam); const avg=counts.reduce((a,b)=>a+b,0)/counts.length; score -= counts.reduce((s,x)=>s+Math.abs(x-avg),0)*1.5; // Quadrant balance based on word centers. const q=[0,0,0,0]; for (const p of placements) { const centerIdx=p.cells[Math.floor((p.cells.length-1)/2)]; const r=rowOf(centerIdx,size), c=colOf(centerIdx,size); const qi=(r>=size/2?2:0)+(c>=size/2?1:0); q[qi]++; } const qavg=placements.length/4; score -= q.reduce((s,x)=>s+Math.abs(x-qavg),0)*1.2; if (answerCells.length) { // Encourage answer letters not all clumped: reward bounding box area. const rows=answerCells.map(i=>rowOf(i,size)), cols=answerCells.map(i=>colOf(i,size)); if (rows.length) { const area=(Math.max(...rows)-Math.min(...rows)+1)*(Math.max(...cols)-Math.min(...cols)+1); score += Math.min(20,area/(size*size)*20); } } score -= (result.stats?.backtracks||0)/5000; return score; } function buildResultFromAttempt(attempt, words, answer, seed) { const chars = attempt.board.chars.slice(); let answerCells=[]; const rng=createRng(seed ^ 0x9E3779B9); if (answer && answer.length) { const f=fillAnswerCells(chars,answer); if (!f.ok) return {ok:false,type:'FINAL_VALIDATION_FAILED',error:f}; answerCells=f.answerCells; } else { const f=fillRandomCells(chars,attempt.board.size,words,rng); if (!f.ok) return {ok:false,type:'FINAL_VALIDATION_FAILED',error:f}; } const result={ ok:true, size:attempt.board.size, chars, words, placements:attempt.placements, answer, answerCells, seed, stats:attempt.stats, board:attempt.board, feasibility:attempt.feasibility }; const validation=validateFinalPuzzle(result); if (!validation.valid) return {ok:false,type:'FINAL_VALIDATION_FAILED',error:validation}; result.validation=validation; result.qualityScore=qualityScore(result); return result; } function generatePuzzle(rawWords, options={}) { const size=Number(options.size||12); const level=options.level||'normal'; const directionKeys=options.directionKeys || LEVELS[level] || LEVELS.normal; const answer=normalizeAnswer(options.answer||''); const validation=validateInput(rawWords,{size,answer:answer.displayText}); if (!validation.valid) return {ok:false,type:'INPUT_INVALID',errors:validation.errors,validation}; const words=validation.words; const answerMode=answer.length>0; const feas=answerMode?answerFeasibility(words,answer,size):normalFeasibility(words,size); if (feas.status==='IMPOSSIBLE') return {ok:false,type:'PROVEN_IMPOSSIBLE',error:feas,validation}; const baseSeed=(options.seed ?? hashSeed(words.map(w=>w.gridText).join('|')+'|'+answer.gridText+'|'+size+'|'+level))>>>0; const attempts=Math.max(1,Number(options.attempts||DEFAULT_ATTEMPTS)); const maxNodes=Number(options.maxNodes||DEFAULT_MAX_NODES); let best=null; const failures=[]; for (let i=0;i<attempts;i++) { const seed=(baseSeed + Math.imul(i+1,0x9E3779B1))>>>0; const attempt=solveAttempt(words,size,directionKeys,{answer,seed,maxNodes}); if (!attempt.ok) { failures.push(attempt); continue; } const result=buildResultFromAttempt(attempt,words,answer,seed); if (!result.ok) { failures.push(result); continue; } if (!best || result.qualityScore>best.qualityScore) best=result; } if (best) return best; const proven=failures.find(f=>f.type==='PROVEN_IMPOSSIBLE'); if (proven) return proven; return {ok:false,type:'SEARCH_EXHAUSTED',failures,validation}; } function getFeasibleSizes(rawWords, options={}) { const words=normalizeWords(rawWords); const answer=normalizeAnswer(options.answer||''); const min=options.min||MIN_GRID_SIZE, max=options.max||MAX_GRID_SIZE; const out=[]; for (let size=min;size<=max;size++) { const f=answer.length?answerFeasibility(words,answer,size):normalFeasibility(words,size); out.push({size,...f}); } return out; } function pickRecommendedSize(rawWords, options={}) { const words=normalizeWords(rawWords); if (!words.length) return 12; const answer=normalizeAnswer(options.answer||''); const min=Math.max(MIN_GRID_SIZE, Math.max(...words.map(w=>w.length))); const max=MAX_GRID_SIZE; if (answer.length) { const candidates=[]; for (let size=min;size<=max;size++) { const f=answerFeasibility(words,answer,size); if (f.status==='POSSIBLE') { // Prefer modest crossing ratio; avoid near theoretical maximum. const ratio=f.C/Math.max(1,f.T); const penalty=Math.abs(ratio-0.12)*100 + Math.max(0,f.C-f.maxCrossings*0.75)*2; candidates.push({size,penalty,f}); } } candidates.sort((a,b)=>a.penalty-b.penalty || b.size-a.size); return candidates[0]?.size ?? min; } const total=words.reduce((s,w)=>s+w.length,0); // target around 55% word-letter density before crossings. const estimated=Math.ceil(Math.sqrt(total/0.55)); return Math.max(min,Math.min(max,estimated)); } function userMessageFor(result, context={}) { if (result?.ok) return {kind:'success',title:'Past goed',text:'De woordzoeker is klaar.'}; const err=result?.error || result?.errors?.[0] || result?.validation?.errors?.[0]; const code=err?.code || (typeof err==='string'?err:null); if (result?.type==='SEARCH_EXHAUSTED') return {kind:'error',title:'Deze combinatie lukt niet',text:'Probeer een andere rastergrootte of een andere versie.'}; switch(code) { case ERROR.NO_WORDS: return {kind:'error',title:'Voeg woorden toe',text:'Zet elk zoekwoord op een nieuwe regel.'}; case ERROR.DUPLICATE_WORD: return {kind:'error',title:'Dubbel woord',text:`‘${err.word}’ staat meer dan één keer in de lijst.`}; case ERROR.SUBSTRING_WORD: return {kind:'error',title:'Woorden overlappen inhoudelijk',text:`‘${err.word}’ zit al helemaal in ‘${err.other}’. Kies één van beide.`}; case ERROR.WORD_TOO_LONG: return {kind:'error',title:'Raster te klein',text:`‘${err.word}’ heeft ${err.length} letters en past niet in ${err.size} × ${err.size}.`}; case ERROR.GRID_TOO_SMALL: return {kind:'error',title:'Raster te klein',text:`Het langste woord heeft ${err.longest} letters. Kies minimaal ${err.longest} × ${err.longest}.`}; case ERROR.ANSWER_GRID_TOO_LARGE: return {kind:'error',title:'Raster te groot voor deze uitkomst',text:'Er blijven dan meer vakjes over dan er letters in de uitkomst staan. Kies een kleiner raster of voeg zoekwoorden toe.'}; case ERROR.ANSWER_TOO_MANY_CROSSINGS: return {kind:'error',title:'Deze combinatie past niet',text:'Er zouden meer kruisingen nodig zijn dan deze woorden kunnen maken. Kies een groter raster of pas de woorden aan.'}; default: return {kind:'error',title:'Geen geldige woordzoeker gevonden',text:'Pas de woorden of rastergrootte aan en probeer opnieuw.'}; } } return { MIN_GRID_SIZE,MAX_GRID_SIZE,DIRECTIONS,LEVELS,ERROR, normalizeWord,normalizeWords,normalizeAnswer,validateInput, answerFeasibility,normalFeasibility,getFeasibleSizes,pickRecommendedSize, createPlacements,createBoard,checkPlacement,applyPlacement,undoPlacement, findOccurrences,validateFinalPuzzle,generatePuzzle,userMessageFor, createRng,hashSeed,indexOfCell,rowOf,colOf,reverseString }; })(); 'use strict'; const root=document.getElementById('kkws-wordsearch-root'); if(!root || root.dataset.kkwsInitialized==='1') return; root.dataset.kkwsInitialized='1'; const $=id=>root.querySelector('#kkws-'+id); const els={ title:$('titleInput'), words:$('wordsInput'), count:$('wordCount'), size:$('sizeRange'), sizeValue:$('sizeValue'), auto:$('autoSize'), status:$('statusBox'), answerEnabled:$('answerEnabled'), answerBox:$('answerBox'), answer:$('answerInput'), showName:$('showName'),showDate:$('showDate'),showGroup:$('showGroup'),includeSolution:$('includeSolution'), gridLines:$('showGridLines'),lowercase:$('lowercaseLetters'),newVersion:$('newVersionBtn'),print:$('printBtn'), stage:$('sheetStage'),meta:$('previewMeta'),directionGrid:$('directionGrid'),printRoot:$('printRoot') }; const state={ level:'normal', seed:(Date.now()>>>0), activeTab:'student', generationId:0, result:null, debounce:null }; function currentWords(){ return els.words.value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean); } function answerText(){ return els.answerEnabled.checked ? els.answer.value.trim() : ''; } function directionKeys(){ return Array.from(els.directionGrid.querySelectorAll('input[data-dir]:checked')).map(x=>x.dataset.dir); } function setDirections(keys){ els.directionGrid.querySelectorAll('input[data-dir]').forEach(cb=>cb.checked=keys.includes(cb.dataset.dir)); } function setLevel(level){ state.level=level; root.querySelectorAll('.level').forEach(b=>b.classList.toggle('active',b.dataset.level===level)); if(WS.LEVELS[level]) setDirections(WS.LEVELS[level]); } function setStatus(kind,title,text,spinning=false){ els.status.className='status '+kind; els.status.innerHTML='<strong'+(spinning?' class="spinning"':'')+'>'+escapeHtml(title)+'</strong><span>'+escapeHtml(text||'')+'</span>'; } function escapeHtml(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));} function updateCount(){ const n=currentWords().length; els.count.textContent=n+' '+(n===1?'woord':'woorden'); } function updateSizeLabel(){ els.sizeValue.textContent=els.size.value+' × '+els.size.value; } function immediateCheck(){ updateCount(); updateSizeLabel(); const words=currentWords(), answer=answerText(); if(!words.length){setStatus('error','Voeg woorden toe','Zet elk zoekwoord op een nieuwe regel.');return false;} if(els.answerEnabled.checked && !answer){setStatus('error','Vul de uitkomst in','Schrijf het woord of de zin die na het doorstrepen moet overblijven.');return false;} if(!directionKeys().length){setStatus('error','Kies een richting','Zet onder Meer instellingen minstens één richting aan.');return false;} let size=Number(els.size.value); if(els.auto.checked){ const recommended=WS.pickRecommendedSize(words,{answer}); if(Number.isFinite(recommended)) { size=recommended; els.size.value=String(size); updateSizeLabel(); } } const v=WS.validateInput(words,{size,answer}); if(!v.valid){ const msg=WS.userMessageFor({ok:false,type:'INPUT_INVALID',errors:v.errors,validation:v});setStatus(msg.kind,msg.title,msg.text);return false; } if(answer){ const feasible=WS.getFeasibleSizes(words,{answer}).filter(x=>x.status==='POSSIBLE'); if(!feasible.length){ const f=WS.answerFeasibility(v.words,v.answer,size); const msg=WS.userMessageFor({ok:false,type:'PROVEN_IMPOSSIBLE',error:f});setStatus(msg.kind,msg.title,msg.text);return false; } if(!els.auto.checked){ const f=WS.answerFeasibility(v.words,v.answer,size); if(f.status==='IMPOSSIBLE'){ const msg=WS.userMessageFor({ok:false,type:'PROVEN_IMPOSSIBLE',error:f});setStatus(msg.kind,msg.title,msg.text);return false; } } } setStatus('info','Wordt gecontroleerd','Klasklaar zoekt een nette plaatsing.',true); return true; } function slimResult(r){ if(!r || !r.ok) return r; return { ok:true, size:r.size, chars:r.chars, words:r.words, placements:r.placements, answer:r.answer, answerCells:r.answerCells, seed:r.seed, stats:r.stats, qualityScore:r.qualityScore, feasibility:r.feasibility }; } function generationSizes(words,opt){ if(!opt.auto) return [opt.size]; const rec=WS.pickRecommendedSize(words,{answer:opt.answer}); if(opt.answer){ return WS.getFeasibleSizes(words,{answer:opt.answer}) .filter(x=>x.status==='POSSIBLE') .map(x=>x.size) .sort((a,b)=>Math.abs(a-rec)-Math.abs(b-rec)); } return [rec,rec+1,rec-1,rec+2,rec-2] .filter((x,i,a)=>Number.isFinite(x)&&x>=6&&x<=25&&a.indexOf(x)===i); } function startGeneration(){ if(!immediateCheck()){ state.result=null; renderPreview(); return; } const id=++state.generationId; const words=currentWords(); const opt={ size:Number(els.size.value), auto:els.auto.checked, level:state.level, directionKeys:directionKeys(), answer:answerText(), seed:state.seed }; const sizes=generationSizes(words,opt); const maxSizes=opt.answer?6:5; const queue=sizes.slice(0,maxSizes); let last=null; let pos=0; if(!queue.length){ handleResult({ok:false,type:'PROVEN_IMPOSSIBLE',error:{code:'ANSWER_GRID_TOO_LARGE'}}); return; } function tryNext(){ if(id!==state.generationId) return; if(pos>=queue.length){ handleResult(last||{ok:false,type:'SEARCH_EXHAUSTED'}); return; } const size=queue[pos++]; setStatus('info','Woordzoeker maken',size+' × '+size+' wordt geprobeerd…',true); // Give the browser a paint moment before the synchronous solver runs. setTimeout(()=>{ if(id!==state.generationId) return; let r; try{ r=WS.generatePuzzle(words,{ size, level:opt.level, directionKeys:opt.directionKeys, answer:opt.answer, seed:opt.seed, attempts:opt.answer?3:5, maxNodes:opt.answer?18000:22000 }); }catch(err){ r={ok:false,type:'ENGINE_ERROR',message:String(err&&err.message||err)}; } if(id!==state.generationId) return; if(r&&r.ok){ handleResult(slimResult(r)); return; } last=r; setTimeout(tryNext,0); },0); } tryNext(); } function handleResult(result){ if(result&&result.ok){ state.result=result; els.size.value=String(result.size);updateSizeLabel(); setStatus('success','Past goed','Alle woorden zijn geplaatst en gecontroleerd.'); els.newVersion.disabled=false;els.print.disabled=false;els.meta.textContent=result.size+' × '+result.size+' · '+result.words.length+' woorden'; }else{ state.result=null;els.newVersion.disabled=true;els.print.disabled=true; const msg=WS.userMessageFor(result||{});setStatus(msg.kind,msg.title,msg.text);els.meta.textContent='Geen geldige puzzel'; } renderPreview(); } function scheduleGeneration(ms=340){clearTimeout(state.debounce);state.debounce=setTimeout(startGeneration,ms);} function cellFontSize(size){ return Math.max(22,Math.min(57,650/size)); } function gridPhysicalMm(size){ return Math.round(94 + (size-6)*(54/19)); } function charForDisplay(ch){return els.lowercase.checked?String(ch).toLocaleLowerCase('nl-NL'):ch;} function renderGridSvg(result,solution=false){ const size=result.size, unit=1000/size, fs=cellFontSize(size), gridLines=els.gridLines.checked; let marks=''; if(solution){ for(const p of result.placements){ const first=p.cells[0],last=p.cells[p.cells.length-1]; const x1=(first%size+.5)*unit,y1=(Math.floor(first/size)+.5)*unit; const x2=(last%size+.5)*unit,y2=(Math.floor(last/size)+.5)*unit; marks+=`<line class="kkws-print-solution-mark" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke-width="${unit*.56}"/>`; } if(result.answer&&result.answer.length){ for(const idx of result.answerCells){const x=(idx%size+.5)*unit,y=(Math.floor(idx/size)+.5)*unit;marks+=`<circle class="kkws-print-answer-ring" cx="${x}" cy="${y}" r="${unit*.33}"/>`;} } } let lines=''; if(gridLines){ for(let i=0;i<=size;i++){const p=i*unit;lines+=`<line class="kkws-print-cell-line" x1="0" y1="${p}" x2="1000" y2="${p}"/><line class="kkws-print-cell-line" x1="${p}" y1="0" x2="${p}" y2="1000"/>`;} } let letters=''; for(let i=0;i<result.chars.length;i++){ const x=(i%size+.5)*unit,y=(Math.floor(i/size)+.5)*unit; letters+=`<text x="${x}" y="${y}" font-size="${fs}">${escapeHtml(charForDisplay(result.chars[i]))}</text>`; } return `<svg class="kkws-print-grid" style="width:${gridPhysicalMm(size)}mm;height:${gridPhysicalMm(size)}mm" viewBox="0 0 1000 1000" role="img" aria-label="Woordzoeker">${marks}${lines}<rect class="kkws-print-outer-line" x="1.7" y="1.7" width="996.6" height="996.6"/>${letters}</svg>`; } function wordListColumns(words){ const max=Math.max(...words.map(w=>w.displayText.length),1), n=words.length; if(max>18)return 2;if(n<=8)return 2;if(n<=18)return 3;return 4; } function answerBlanks(answer){ if(!answer||!answer.length)return''; const groups=answer.groups&&answer.groups.length?answer.groups:[answer.length]; return groups.map(n=>Array(n).fill('_').join(' ')).join(' '); } function studentFieldsHtml(){ const field=(label,width)=>`<span class="kkws-print-student-field"><span class="kkws-print-student-label">${label}</span><span class="kkws-print-student-line" style="min-width:${width}mm"></span></span>`; const f=[];if(els.showName.checked)f.push(field('Naam',42)); if(els.showGroup.checked)f.push(field('Groep',24)); if(els.showDate.checked)f.push(field('Datum',28)); return f.length?`<div class="kkws-print-student-row">${f.join('')}</div>`:''; } function sheetHtml(result,solution=false){ const cols=wordListColumns(result.words); const instr=result.answer&&result.answer.length?'Zoek de woorden en streep ze door. Welke letters blijven over?':'Zoek alle woorden in de woordzoeker.'; const words=result.words.map(w=>`<div class="kkws-print-worditem">${escapeHtml(w.displayText)}</div>`).join(''); const answerBlock=result.answer&&result.answer.length?(solution?`<div class="kkws-print-solution-answer"><span class="kkws-print-answer-label">Uitkomst</span><span class="kkws-print-solution-answer-value">${escapeHtml(result.answer.displayText)}</span></div>`:`<div class="kkws-print-answer-line"><span class="kkws-print-answer-label">Uitkomst</span><span class="kkws-print-answer-blanks">${escapeHtml(answerBlanks(result.answer))}</span></div>`):''; const brand=`<div class="kkws-print-logo-box"><img class="kkws-print-logo" alt="Klasklaar" src="data:image/png;base64,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"></div>`; const head=`<div class="kkws-print-header"><div class="kkws-print-heading"><div class="kkws-print-kicker">${solution?'Antwoordblad':'Woordzoeker'}</div><div class="kkws-print-title">${escapeHtml(els.title.value.trim()||'Woordzoeker')}</div></div>${brand}</div>`; const wordBlock=`<div class="kkws-print-wordblock"><div class="kkws-print-wordblock-head"><span class="kkws-print-wordblock-mark"></span><div class="kkws-print-wordlist-title">${solution?'Gezochte woorden':'Zoek deze woorden'}</div></div><div class="kkws-print-wordlist" style="grid-template-columns:repeat(${cols},minmax(0,1fr))">${words}</div></div>`; return `<section class="kkws-print-page ${solution?'kkws-solution-page':'kkws-student-page'}"><div class="kkws-print-content">${head}${studentFieldsHtml()}<div class="kkws-print-instruction">${escapeHtml(solution?'De gevonden woorden zijn gemarkeerd.':instr)}</div><div class="kkws-print-grid-holder">${renderGridSvg(result,solution)}</div>${wordBlock}${answerBlock}<div class="kkws-print-footer">Klasklaar.nl</div></div></section>`; } function fitSheet(){ const wrap=els.stage.querySelector('.kkws-preview-page-wrap'); const sheet=wrap&&wrap.querySelector('.kkws-print-page'); if(!wrap||!sheet)return; const stageWidth=els.stage.clientWidth; if(stageWidth<50){setTimeout(fitSheet,60);return;} // Measure the real 210 × 297 mm page before scaling. Never assume a fixed px/mm conversion. wrap.style.transform='none'; wrap.style.left='0px'; const baseW=sheet.offsetWidth||sheet.getBoundingClientRect().width; const baseH=sheet.offsetHeight||sheet.getBoundingClientRect().height; if(!baseW||!baseH)return; const avail=Math.max(1,stageWidth-12); // De preview groeit mee met de browser. Op brede laptops/digiborden mag A4 bewust groter dan 100% worden weergegeven. const scale=Math.min(1.28,avail/baseW); const renderedW=baseW*scale; wrap.style.left=Math.max(0,(stageWidth-renderedW)/2)+'px'; wrap.style.transform=`scale(${scale})`; els.stage.style.height=Math.ceil(baseH*scale+12)+'px'; } function renderPreview(){ if(!state.result){els.stage.innerHTML='<div class="empty-preview"><div><strong>Nog geen geldige woordzoeker</strong>Pas de woorden of instellingen aan.</div></div>';return;} const solution=state.activeTab==='solution'; els.stage.innerHTML=`<div class="kkws-preview-page-wrap">${sheetHtml(state.result,solution)}</div>`; requestAnimationFrame(fitSheet); renderPrintRoot(); } function renderPrintRoot(){ if(!state.result){els.printRoot.innerHTML='';return;} els.printRoot.innerHTML=sheetHtml(state.result,false)+(els.includeSolution.checked?sheetHtml(state.result,true):''); } function rerenderOnly(){if(state.result){renderPreview();}else renderPrintRoot();} // Events els.title.addEventListener('input',rerenderOnly); els.words.addEventListener('input',()=>scheduleGeneration()); els.answerEnabled.addEventListener('change',()=>{els.answerBox.classList.toggle('show',els.answerEnabled.checked);scheduleGeneration(100)}); els.answer.addEventListener('input',()=>scheduleGeneration()); els.auto.addEventListener('change',()=>scheduleGeneration(80)); els.size.addEventListener('input',()=>{els.auto.checked=false;updateSizeLabel();immediateCheck();clearTimeout(state.debounce)}); els.size.addEventListener('change',()=>scheduleGeneration(50)); root.querySelectorAll('.level').forEach(btn=>btn.addEventListener('click',()=>{setLevel(btn.dataset.level);scheduleGeneration(80)})); els.directionGrid.addEventListener('change',()=>{state.level='custom';root.querySelectorAll('.level').forEach(b=>b.classList.remove('active'));scheduleGeneration(80)}); [els.showName,els.showDate,els.showGroup,els.includeSolution,els.gridLines,els.lowercase].forEach(el=>el.addEventListener('change',rerenderOnly)); els.newVersion.addEventListener('click',()=>{state.seed=(state.seed+0x9E3779B9)>>>0;scheduleGeneration(10)}); function printWorksheet(){ renderPrintRoot(); if(!state.result)return; // Build an independent print layer before detaching anything from JouwWeb. const printLayer=document.createElement('div'); printLayer.id='kkws-print-sandbox'; printLayer.className='kkws-doc'; printLayer.innerHTML=els.printRoot.innerHTML; // The embed stylesheet normally lives inside the page body. Clone it into <head> so // the worksheet keeps its exact styling while all JouwWeb body nodes are temporarily removed. const sourceStyle=document.getElementById('kkws-wordsearch-style'); const tempStyle=document.createElement('style'); tempStyle.id='kkws-wordsearch-temporary-print-style'; tempStyle.textContent=(sourceStyle?sourceStyle.textContent:'')+'\n'+ 'body.kkws-print-isolated{margin:0!important;padding:0!important;background:#fff!important;}'+ '#kkws-print-sandbox{display:block!important;margin:0!important;padding:0!important;width:210mm!important;}'+ '#kkws-print-sandbox .kkws-print-page{width:210mm!important;min-width:210mm!important;max-width:210mm!important;height:297mm!important;min-height:297mm!important;max-height:297mm!important;box-shadow:none!important;}'; document.head.appendChild(tempStyle); const body=document.body; const oldClass=body.className; const oldStyle=body.getAttribute('style'); const stash=document.createDocumentFragment(); // childNodes (not children!) intentionally includes loose text nodes such as a JouwWeb ${body} placeholder. Array.from(body.childNodes).forEach(node=>stash.appendChild(node)); body.className='kkws-print-isolated'; body.removeAttribute('style'); body.appendChild(printLayer); let cleaned=false; const cleanup=()=>{ if(cleaned)return; cleaned=true; try{printLayer.remove();}catch(_e){} while(body.firstChild)body.removeChild(body.firstChild); body.appendChild(stash); body.className=oldClass; if(oldStyle===null)body.removeAttribute('style');else body.setAttribute('style',oldStyle); tempStyle.remove(); requestAnimationFrame(fitSheet); }; window.addEventListener('afterprint',cleanup,{once:true}); setTimeout(()=>{ try{window.print();} catch(err){if(window.console&&console.error)console.error('Klasklaar print:',err);} finally{setTimeout(cleanup,0);} },80); } els.print.addEventListener('click',printWorksheet); root.querySelectorAll('.tab').forEach(t=>t.addEventListener('click',()=>{state.activeTab=t.dataset.tab;root.querySelectorAll('.tab').forEach(x=>x.classList.toggle('active',x===t));renderPreview()})); window.addEventListener('resize',()=>requestAnimationFrame(fitSheet)); updateCount();updateSizeLabel();els.answerBox.classList.toggle('show',els.answerEnabled.checked);scheduleGeneration(60); }catch(err){ const root=document.getElementById('kkws-wordsearch-root'); const status=root&&root.querySelector('#kkws-statusBox'); const stage=root&&root.querySelector('#kkws-sheetStage'); if(status){ status.className='status error'; status.innerHTML='<strong>De woordzoekermaker kon niet starten</strong><span>Ververs de pagina. Blijft dit gebeuren, controleer dan of aangepaste JavaScript-code is toegestaan.</span>'; } if(stage){ stage.innerHTML='<div class="empty-preview"><div><strong>De preview kon niet worden gestart</strong>De puzzelcode is door de pagina geblokkeerd.</div></div>'; } if(typeof console!=='undefined'&&console.error) console.error('Klasklaar woordzoekermaker:',err); } })();
${body}
${SITE_TEXT}
${rows}