/* MIT License Copyright (c) 2022 Viresh Ratnakar See the full Exet license notice in exet.js. Current version: v1.03, December 26, 2025 */ /** * ExetDher is an implementation of a double heap that stores the top k * candidates in terms of descending scores. A min-heap lets us retain * the top-k elements, while a max-heap lets us extract the max element. * The candidate objects stored in this (via add()) should have a "score" field. * Objects stored in this get a property named "dher" attached to them, for * storing some book-keping info. */ class ExetDher { constructor(lim) { this.maxelts = []; this.minelts = []; this.lim = lim; console.assert(lim > 0, lim); } relimit(lim) { console.assert(lim > 0, lim); const extra = this.minelts.length - lim; for (let i = 0; i < extra; i++) { this.pop(false, 0); } this.lim = lim; } size() { return this.maxelts.length; } limit() { return this.lim; } parent(idx) { return idx == 0 ? 0 : (idx - 1) >> 1; } heapifyUp(inMax, idx) { let elts = inMax ? this.maxelts : this.minelts; console.assert(idx >= 0 && idx < elts.length, idx, elts.length); while (idx > 0) { let parent = this.parent(idx); if (inMax) { if (elts[parent].score >= elts[idx].score) { return; } } else { if (elts[parent].score <= elts[idx].score) { return; } } const temp = elts[idx]; elts[idx] = elts[parent]; elts[parent] = temp; elts[idx].dher[inMax] = idx; elts[parent].dher[inMax] = parent; idx = parent; } } heapifyDown(inMax, idx) { let elts = inMax ? this.maxelts : this.minelts; console.assert(idx >= 0 && idx < elts.length, idx, elts.length); while (idx < elts.length) { let lchild = (idx << 1) + 1; if (lchild >= elts.length) return; let child = lchild; let rchild = lchild + 1; if (inMax) { if (rchild < elts.length && elts[rchild].score > elts[lchild].score) { child = rchild; } if (elts[idx].score >= elts[child].score) return; } else { if (rchild < elts.length && elts[rchild].score < elts[lchild].score) { child = rchild; } if (elts[idx].score <= elts[child].score) return; } let temp = elts[idx]; elts[idx] = elts[child]; elts[child] = temp; elts[idx].dher[inMax] = idx; elts[child].dher[inMax] = child; idx = child; } } add(candidate) { let num = this.minelts.length; if (num == this.lim) { let worst = this.minelts[0]; if (candidate.score < worst.score) { return; } if (candidate.score == worst.score) { if (Math.random() < 0.5) { return; } } this.pop(false); num--; } candidate.dher = {}; candidate.dher[true] = num; candidate.dher[false] = num; this.maxelts.push(candidate); this.minelts.push(candidate); this.heapifyUp(true, num); this.heapifyUp(false, num); } popInner(inMax, idx) { const elts = inMax ? this.maxelts : this.minelts; const len = elts.length; console.assert(len > 0, len); const last = elts.pop(); if (len == 1 || idx >= len - 1) { return last; } /* len > 1 && idx < len - 1 */ const ret = elts[idx]; elts[idx] = last; last.dher[inMax] = idx; const parent = this.parent(idx); if (idx > 0 && ((inMax && last.score > elts[parent].score) || (!inMax && last.score < elts[parent].score))) { this.heapifyUp(inMax, idx); } else { this.heapifyDown(inMax, idx); } return ret; } pop(inMax, idx=0) { const elts = inMax ? this.maxelts : this.minelts; if (idx < 0 || idx >= elts.length) return null; const otherIdx = elts[idx].dher[!inMax]; this.popInner(!inMax, otherIdx); return this.popInner(inMax, idx); } peep(inMax, idx=0) { const elts = inMax ? this.maxelts : this.minelts; if (idx < 0 || idx >= elts.length) return null; return elts[idx]; } } /** * A data structure encapsulating clues and a grid, along with available fill * choices. Used for figuring out viability, weeding out non-viable choices, * and doing autofill. Note that you can initialize this from exet.puz as well * as from another ExetFillState. */ function ExetFillState(obj) { this.gridWidth = obj.gridWidth; this.gridHeight = obj.gridHeight; this.grid = new Array(this.gridHeight); this.viable = obj.viable ?? true; for (let i = 0; i < this.gridHeight; i++) { this.grid[i] = new Array(this.gridWidth); for (let j = 0; j < this.gridWidth; j++) { const gridCell = obj.grid[i][j]; this.grid[i][j] = { ...gridCell }; const thisCell = this.grid[i][j]; for (const prop in thisCell) { if (thisCell[prop] instanceof Node) { delete thisCell[prop]; } } if (!thisCell.isLight) { continue; } if (!thisCell.cChoices) { thisCell.cChoices = {}; } } } this.clues = {}; for (let ci in obj.clues) { const theClue = obj.clues[ci]; this.clues[ci] = { ...theClue }; const thisClue = this.clues[ci]; for (const prop in thisClue) { if (thisClue[prop] instanceof Node) { delete thisClue[prop]; } } if (!thisClue.childrenClueIndices) { thisClue.childrenClueIndices = []; } if (!thisClue.lChoices) { thisClue.lChoices = []; } if (!thisClue.lRejects) { thisClue.lRejects = []; } } this.delta = []; /** preflexUsed and numPreflexUsed are updated in exet.refineLightChoices() */ this.preflexUsed = obj.preflexUsed || {}; this.numPreflexUsed = obj.numPreflexUsed || 0; } ExetFillState.prototype.markClueEnds = function() { for (let r = 0; r < this.gridHeight; r++) { for (let c = 0; c < this.gridWidth; c++) { const gridCell = this.grid[r][c]; if (!gridCell.isLight) { continue; } if (gridCell.startsAcrossClue) { const last = gridCell.startsAcrossClue[gridCell.startsAcrossClue.length - 1]; const lastCell = this.grid[last[0]][last[1]]; lastCell.endsAcrossClue = gridCell.startsClueLabel; } if (gridCell.startsDownClue) { const last = gridCell.startsDownClue[gridCell.startsDownClue.length - 1]; const lastCell = this.grid[last[0]][last[1]]; lastCell.endsDownClue = gridCell.startsClueLabel; } if (gridCell.startsZ3dClue) { const last = gridCell.startsZ3dClue[gridCell.startsZ3dClue.length - 1]; const lastCell = this.grid[last[0]][last[1]]; lastCell.endsZ3dClue = gridCell.startsClueLabel; } } } } ExetFillState.prototype.initViability = function() { this.viable = true; for (let i = 0; i < this.gridHeight; i++) { for (let j = 0; j < this.gridWidth; j++) { const fillCell = this.grid[i][j]; if (!fillCell.isLight) { continue; } if (fillCell.solution != '?') { fillCell.cChoices = {}; fillCell.cChoices[fillCell.solution] = true; fillCell.viability = 1.0; } else { fillCell.cChoices = exetLexicon.letterSet; fillCell.viability = 5.0; } } } } /** * Fills lChoices in each clue and initializes cChoices in each cell. */ ExetFillState.prototype.resetViability = function() { this.initViability(); const dontReuse = {}; this.preflexUsed = {}; this.numPreflexUsed = 0; for (const ci in this.clues) { const theClue = this.clues[ci]; if (!theClue.solution || theClue.solution.indexOf('?') >= 0) { continue; } const choices = exetLexicon.getLexChoices(theClue.solution, 1, dontReuse, exet.noProperNouns, exet.indexMinPop, false, exet.preflexByLen, exet.unpreflexSet, exet.getLightRegexpC(ci)); theClue.lChoices = choices; theClue.lRejects = []; if (choices.length > 0) { const p = choices[0]; console.assert(p > 0, p); exet.addToDontReuse(p, dontReuse); if (exet.preflexSet[p]) { this.preflexUsed[p] = true; this.numPreflexUsed++; } } } for (const ci in this.clues) { const theClue = this.clues[ci]; if (!theClue.solution || theClue.solution.indexOf('?') < 0) { continue; } theClue.lChoices = exetLexicon.getLexChoices(theClue.solution, 0, dontReuse, exet.noProperNouns, exet.indexMinPop, exet.tryReversals, exet.preflexByLen, exet.unpreflexSet, exet.getLightRegexpC(ci)); theClue.lRejects = []; } } /** * autofill should be an ExetAutofill object. */ ExetFillState.prototype.setScore = function(autofill) { this.scoreF = 0; /* fullness */ this.scoreV = 0; /* viability */ this.scoreP = 0; /* popularity */ this.score = 0; this.unfilled = []; this.lettersUsed = {}; this.constrLetters = {}; this.reversals = 0; let numEntries = 0; for (const ci in this.clues) { const theClue = this.clues[ci]; if (theClue.lChoices.length == 1 && theClue.lChoices[0] < 0) { this.reversals++; } if (!theClue.parentClueIndex) { let scoreP = 0; /* we use popularity of the first choice */ if (theClue.lChoices.length > 0) { let pindex = theClue.lChoices[0]; if (pindex < 0) pindex = 0 - pindex; if (pindex >= exetLexicon.startLen || this.preflexUsed[pindex]) { /* we really prefer preflex entries */ scoreP = 1.0; } else { scoreP = (exetLexicon.startLen - pindex) / exetLexicon.startLen; } } ++numEntries; this.scoreP += scoreP; } } if (numEntries > 0) { this.scoreP /= numEntries; } this.score += this.scoreP; let numLightCells = 0; for (let i = 0; i < this.gridHeight; i++) { for (let j = 0; j < this.gridWidth; j++) { const fillCell = this.grid[i][j]; if (!fillCell.isLight) { continue } numLightCells++; if (fillCell.solution != '?' || fillCell.currLetter != '?') { let c = fillCell.solution; if (c == '?') { c = fillCell.currLetter; } console.assert(c, i, j, fillCell); this.lettersUsed[c] = true; if (this.pangramCell(i, j, autofill)) { this.constrLetters[c] = true; } continue; } if (fillCell.viability <= 0) { this.unfilled.push([i, j, fillCell.viability]); this.scoreV = - Number.MAX_VALUE; this.score = this.scoreV; this.viable = false; return; } this.scoreV += Math.log(fillCell.viability); this.unfilled.push([i, j, fillCell.viability]); } } this.numLettersUsed = Object.keys(this.lettersUsed).length; const constrUsed = Object.keys(this.constrLetters); this.numConstrLetters = constrUsed.length; if (numLightCells == 0) { return; } let boost = 0 if (autofill.boostPangram) { for (const c of constrUsed) { boost += exetLexicon.letterRarity(c); } } if (autofill.boostPangram && constrUsed.length < exetLexicon.letters.length) { // Sort this.unfilled by ascending frequency of unused letter choices, // then by ascending viability. But add a little random salt to the rarity, // to avoid favouring low-numbered cells. for (const x of this.unfilled) { if (!this.pangramCell(x[0], x[1], autofill)){ x.push(0); continue; } const cell = this.grid[x[0]][x[1]]; const choices = Object.keys(cell.cChoices); let maxRarity = 0; for (let c of choices) { if (this.constrLetters[c]) { continue; } const rarity = exetLexicon.letterRarity(c); if (rarity > maxRarity) maxRarity = rarity; } x.push(maxRarity > 0 ? (maxRarity + 0.1 * Math.random()) : 0); } this.unfilled.sort((a, b) => a[3] == b[3] ? a[2] - b[2] : b[3] - a[3]); } else { this.unfilled.sort((a, b) => a[2] - b[2]); } this.scoreV /= 100; this.score += this.scoreV; const f = numLightCells - this.unfilled.length; const progressWeight = 30; this.scoreF = progressWeight * (f + boost) / 100; this.score += this.scoreF; } /** * Is [r,c] a cell to be counted for a constrained pangram? * autofill should be an ExetAutofill object. */ ExetFillState.prototype.pangramCell = function(r, c, autofill) { const gridCell = this.grid[r][c]; if (!gridCell.isLight) { return false; } if (autofill.pangramAll) { return true; } if (autofill.pangramCircled && gridCell.hasCircle) { return true; } let numLights = 0; if (gridCell.acrossClueLabel) numLights++; if (gridCell.downClueLabel) numLights++; if (gridCell.z3dClueLabel) numLights++; if (autofill.pangramChecked && numLights > 1) { return true; } if (autofill.pangramUnchecked && numLights == 1) { return true; } if (!autofill.pangramFirsts && !autofill.pangramLasts) { return false; } const firsts = []; if (gridCell.startsAcrossClue) firsts.push('A' + gridCell.startsClueLabel); if (gridCell.startsDownClue) firsts.push('D' + gridCell.startsClueLabel); if (gridCell.startsZ3dClue) firsts.push('Z' + gridCell.startsClueLabel); const lasts = []; if (autofill.pangramLasts || exet.tryReversals) { if (gridCell.endsAcrossClue) lasts.push('A' + gridCell.endsAcrossClue); if (gridCell.endsDownClue) lasts.push('D' + gridCell.endsDownClue); if (gridCell.endsZ3dClue) lasts.push('Z' + gridCell.endsZ3dClue); } if (exet.tryReversals && (!autofill.pangramFirsts || !autofill.pangramLasts)) { const realFirsts = []; const revFirsts = []; for (const ci of firsts) { const clue = this.clues[ci]; if (clue && clue.lChoices.length == 1 && clue.lChoices[0] < 0) { revFirsts.push(ci); } else { realFirsts.push(ci); } } const realLasts = []; const revLasts = []; for (const ci of lasts) { const clue = this.clues[ci]; if (clue && clue.lChoices.length == 1 && clue.lChoices[0] < 0) { revLasts.push(ci); } else { realLasts.push(ci); } } firsts = realFirsts.concat(revLasts); lasts = realLasts.concat(revFirsts); } if (autofill.pangramFirsts && firsts.length > 0) { return true; } if (autofill.pangramLasts && lasts.length > 0) { return true; } return false; } ExetFillState.prototype.isFull = function() { return this.unfilled.length == 0; } ExetFillState.prototype.hash = function() { let fills = ''; for (let i = 0; i < this.gridHeight; i++) { for (let j = 0; j < this.gridWidth; j++) { const gridCell = this.grid[i][j]; if (!gridCell.isLight) continue; fills += gridCell.currLetter || '?'; } } this.fills = fills; return exetLexicon.javaHash(fills); } ExetFillState.prototype.getCurrEntry = function(ci) { const cells = exet.puz.getAllCells(ci, this.clues); let entry = ''; for (const cell of cells) { const gridCell = this.grid[cell[0]][cell[1]]; entry += gridCell.currLetter; } return entry; } ExetFillState.prototype.hasPatternOfDeath = function() { for (let i = 0; i < this.gridHeight; i++) { for (let j = 0; j < this.gridWidth; j++) { const gridCell = this.grid[i][j]; if (!gridCell.isLight) continue; if (gridCell.currLetter != '?') continue; if (!gridCell.acrossClueLabel || !gridCell.downClueLabel) continue; let aci = exet.puz.getDirClueIndex('A', gridCell.acrossClueLabel); let dci = exet.puz.getDirClueIndex('D', gridCell.downClueLabel); console.assert(aci && dci, aci, dci); let ac = this.clues[aci]; if (ac.parentClueIndex) { aci = ac.parentClueIndex; ac = this.clues[aci]; } let dc = this.clues[dci]; if (dc.parentClueIndex) { dci = dc.parentClueIndex; dc = this.clues[dci]; } if (ac.solution != dc.solution) { continue; } const acEntry = this.getCurrEntry(aci); const dcEntry = this.getCurrEntry(dci); if (acEntry != dcEntry) { continue; } const loc = acEntry.indexOf('?'); console.assert(loc >= 0, acEntry); if (acEntry.substr(0, loc).indexOf('?') >= 0 || acEntry.substr(loc + 1).indexOf('?') >= 0) { continue; } /* Across and down solutions are identical, and have the * current cell as the only unfilled cell. This is not * fillable! */ return true; } } return false; } /** * Only set cChoice and lChoice in exet.fillState, so that * picked suggesstions can be shown in gray. */ ExetFillState.prototype.updateExetFill = function() { // Show light-fill suggestions from full lights. for (const ci in this.clues) { const lChoices = this.clues[ci].lChoices; if (lChoices.length != 1) { continue; } const theClue = exet.fillState.clues[ci]; console.assert(theClue, ci); theClue.lChoices = lChoices.slice(); theClue.lRejects = this.clues[ci].lRejects.slice(); } // Show grid-cell suggestions. for (let row = 0; row < this.gridHeight; row++) { for (let col = 0; col < this.gridWidth; col++) { const gCell = this.grid[row][col]; if (!gCell.isLight) { continue; } const choices = gCell.cChoices; if (Object.keys(choices).length == 1) { exet.fillState.grid[row][col].cChoices = { ...choices }; } } } exet.updateViablots(); } /** * Use the global "exet" to access the Exet object from ExetAutofill. */ function ExetAutofill() { this.id = exet.puz.id; this.candidates = []; this.beamWidth = 64; this.beam = new ExetDher(64); this.step = 0; this.numCells = exet.puz.gridWidth * exet.puz.gridHeight * exet.puz.layers3d; this.running = false; this.throttledTimer = null; this.lag = 200; this.msUsed = 0; this.status = 'None'; this.boostPangram = false; this.loopForPangram = false; this.pangramAll = true; this.pangramCircled = false; this.pangramChecked = false; this.pangramUnchecked = false; this.pangramFirsts = false; this.pangramLasts = false; this.triedHashes = {}; /** How many light choices do we consider for each light: */ this.constrainerLimit = 2000; /** How many iterations of refineLightChoices to vet: */ this.refinementSweeps = 2; /** How many loops to try to use preflex */ this.priorityLoops = 20; const analysis = new ExetAnalysis( exet.puz.grid, exet.puz.gridWidth, exet.puz.gridHeight, exet.puz.layers3d); this.barred = analysis.numBars() > 0; this.doublyChecked = analysis.unchequeredOK(false); this.accept = document.getElementById("xet-autofill-accept"); this.clear = document.getElementById("xet-autofill-clear"); this.startstopButton = document.getElementById("xet-autofill-startstop"); this.activeDiv = document.getElementById('xet-autofill-active'); this.activeDivMsg = document.getElementById('xet-autofill-active-msg'); this.clear.disabled = true; this.clear.addEventListener('click', e => { this.accept.disabled = true; this.clear.disabled = true; this.reset('Cleared'); exet.resetViability(); this.refreshDisplay(); this.activeDiv.style.display = 'none'; }) this.accept.disabled = true; this.accept.addEventListener('click', e => { this.accept.disabled = true; this.clear.disabled = true; exet.acceptAll(); this.activeDiv.style.display = 'none'; }) if (this.running) { this.startstopButton.innerText = 'Pause'; this.startstopButton.className = 'xlv-button xet-pink-button'; } this.startstopButton.addEventListener( 'click', this.startstop.bind(this)); this.beamWidthInp = document.getElementById( 'xet-autofill-max-beam'); this.beamWidthInp.value = this.beamWidth; this.pangramInp = document.getElementById( 'xet-autofill-boost-pangram'); this.pangramInp.checked = this.boostPangram; this.pangramLoopInp = document.getElementById( 'xet-autofill-pangram-loop'); this.pangramLoopInp.checked = this.loopForPangram; this.pangramDetails = document.getElementById( 'xet-autofill-pangram-details'); this.pangramAllInp = document.getElementById( 'xet-autofill-pangram-all'); this.pangramAllInp.checked = this.pangramAll; this.pangramCircledInp = document.getElementById( 'xet-autofill-pangram-circled'); this.pangramCircledInp.checked = this.pangramCircled; this.pangramCheckedInp = document.getElementById( 'xet-autofill-pangram-checked'); this.pangramCheckedInp.checked = this.pangramChecked; this.pangramUncheckedInp = document.getElementById( 'xet-autofill-pangram-unchecked'); this.pangramUncheckedInp.checked = this.pangramUnchecked; this.pangramFirstsInp = document.getElementById( 'xet-autofill-pangram-firsts'); this.pangramFirstsInp.checked = this.pangramFirsts; this.pangramLastsInp = document.getElementById( 'xet-autofill-pangram-lasts'); this.pangramLastsInp.checked = this.pangramLasts; const pangramOptionsSanitizer = (e) => { this.pangramAllInp.checked = (!this.pangramCircledInp.checked && !this.pangramCheckedInp.checked && !this.pangramUncheckedInp.checked && !this.pangramFirstsInp.checked && !this.pangramLastsInp.checked) || (this.pangramCheckedInp.checked && this.pangramUncheckedInp.checked); }; for (const elt of [this.pangramCircledInp, this.pangramCheckedInp, this.pangramUncheckedInp, this.pangramFirstsInp, this.pangramLastsInp]) { elt.addEventListener('change', pangramOptionsSanitizer); } this.pangramAllInp.addEventListener('change', (e) => { if (this.pangramAllInp.checked) { this.pangramCircledInp.checked = false; this.pangramCheckedInp.checked = false; this.pangramUncheckedInp.checked = false; this.pangramFirstsInp.checked = false; this.pangramLastsInp.checked = false; } else { this.pangramAllInp.checked = (!this.pangramCircledInp.checked && !this.pangramCheckedInp.checked && !this.pangramUncheckedInp.checked && !this.pangramFirstsInp.checked && !this.pangramLastsInp.checked) || (this.pangramCheckedInp.checked && this.pangramUncheckedInp.checked); } }); this.stepSpan = document.getElementById('xet-autofill-step'); this.stepSpan.innerText = this.step; this.statusSpan = document.getElementById('xet-autofill-status'); this.statusSpan.innerHTML = this.status; this.timeSpan = document.getElementById('xet-autofill-time'); this.speedSpan = document.getElementById('xet-autofill-speed'); this.currBeamSpan = document.getElementById('xet-autofill-curr-beam'); this.currBeamSpan.innerText = this.beam.limit(); this.scoreSpan = document.getElementById('xet-autofill-score'); this.scoreVSpan = document.getElementById('xet-autofill-score-v'); this.scorePSpan = document.getElementById('xet-autofill-score-p'); this.scoreFSpan = document.getElementById('xet-autofill-score-f'); this.reversalsSpan = document.getElementById('xet-autofill-reversals'); this.preflexTotalSpan = document.getElementById( 'xet-autofill-preflex-total'); this.preflexUsedSpan = document.getElementById( 'xet-autofill-preflex-used'); this.unpreflexTotalSpan = document.getElementById( 'xet-autofill-unpreflex-total'); this.minpopSpan = document.getElementById('xet-autofill-minpop'); this.indexMinPopSpan = document.getElementById( 'xet-autofill-index-minpop'); this.properNounsSpan = document.getElementById( 'xet-autofill-proper-nouns'); this.stemDupesSpan = document.getElementById( 'xet-autofill-stem-dupes'); this.tryReversalsSpan = document.getElementById( 'xet-autofill-try-reversals'); this.pangramSpan = document.getElementById('xet-autofill-letters'); this.pangramConstrSpan = document.getElementById( 'xet-autofill-pangram-cletters'); this.pangramConstrSpan.style.display = 'none'; this.isPangram = document.getElementById('xet-is-pangram'); this.refreshDisplay(); } ExetAutofill.prototype.reset = function(status, longStatus='') { this.beam = new ExetDher(this.beamWidth); this.step = 0; this.msUsed = 0; this.triedHashes = {}; if (!this.running) { return; } if (this.throttledTimer) { clearTimeout(this.throttledTimer); this.throttledTimer = null; } this.status = status; this.statusSpan.innerHTML = longStatus || status; this.activeDivMsg.innerHTML = status; this.running = false; exet.updateSweepInd(); this.startstopButton.innerText = 'Start'; this.startstopButton.className = 'xlv-button'; } ExetAutofill.prototype.startstop = function() { if (!this.running) { if (exet.puz.numCellsToFill == exet.puz.numCellsFilled) { alert('The grid is already full'); return; } const beamWidth = parseInt(this.beamWidthInp.value); if (isNaN(beamWidth) || beamWidth <= 0) { this.beamWidthInp.value = this.beamWidth; } else { this.beamWidth = beamWidth; this.beam.relimit(beamWidth); } this.boostPangram = this.pangramInp.checked; this.loopForPangram = this.pangramLoopInp.checked; this.pangramAll = this.pangramAllInp.checked; this.pangramCircled = this.pangramCircledInp.checked; this.pangramChecked = this.pangramCheckedInp.checked; this.pangramUnchecked = this.pangramUncheckedInp.checked; this.pangramFirsts = this.pangramFirstsInp.checked; this.pangramLasts = this.pangramLastsInp.checked; if (this.beam.size() == 0) { const candidate = new ExetFillState(exet.fillState); for (let s = 0; s < this.refinementSweeps && candidate.viable; s++) { if (!exet.refineLightChoices(candidate, this.constrainerLimit)) break; } candidate.setScore(this); if (!candidate.viable) { alert('Autofill will not work on the current grid. Perhaps retry ' + 'after clearing some constraining lights or modifying the grid?'); return; } this.priorityClues = this.getPriorityClues(candidate); this.priorityLoop = 0; this.beam.add(candidate); this.currBeamSpan.innerText = this.beam.size(); } if (exet.viabilityUpdateTimer) { clearTimeout(exet.viabilityUpdateTimer); exet.viabilityUpdateTimer = null; } this.reshowSettings(); this.running = true; this.status = 'Running'; this.accept.disabled = true; this.clear.disabled = true; exet.sweepIndicator.className = 'xet-sweeping-animated'; this.startstopButton.innerText = 'Pause'; this.startstopButton.className = 'xlv-button xet-pink-button'; if (this.throttledTimer) { clearTimeout(this.throttledTimer); } this.throttledTimer = setTimeout(() => { this.beamSearchStep(); }, this.lag); } else { this.running = false; exet.updateSweepInd(); this.startstopButton.innerText = 'Start'; this.startstopButton.className = 'xlv-button'; this.status = 'Paused'; this.accept.disabled = false; this.clear.disabled = false; clearTimeout(this.throttledTimer); this.throttledTimer = null; } this.statusSpan.innerHTML = this.status; this.activeDivMsg.innerHTML = this.status; this.activeDiv.style.display = 'block'; } ExetAutofill.prototype.reshowSettings = function() { this.preflexTotalSpan.innerText = exet.preflex.length; this.unpreflexTotalSpan.innerText = exet.unpreflex.length; this.minpopSpan.innerText = exet.minpop; this.indexMinPopSpan.innerText = Number( exet.indexMinPop - 1).toLocaleString(); this.properNounsSpan.innerText = exet.noProperNouns ? "disallowed" : "allowed"; this.stemDupesSpan.innerText = exet.noStemDupes ? "disallowed" : "allowed"; this.tryReversalsSpan.innerText = exet.tryReversals ? "allowed" : "disallowed"; } ExetAutofill.prototype.refreshDisplay = function() { this.pangramConstrSpan.style.display = this.pangramAll ? 'none' : ''; this.isPangram.style.display = 'none'; let candidate = this.beam.peep(true); if (!candidate) { candidate = new ExetFillState(exet.fillState); candidate.setScore(this); } this.pangramDetails.open = this.boostPangram && !this.pangramAll; this.preflexUsedSpan.innerText = candidate.numPreflexUsed; this.pangramSpan.innerText = candidate.numLettersUsed; this.pangramConstrSpan.innerText = `(${candidate.numConstrLetters} in pangram cells)`; if (candidate.numLettersUsed == exetLexicon.letters.length) { let isPangram = 'Pangram!'; if (!this.pangramAll && candidate.numConstrLetters == exetLexicon.letters.length) { isPangram = 'Pangram with constraints!';; } this.isPangram.innerHTML = isPangram; this.isPangram.style.display = ''; } this.scoreSpan.innerText = candidate.score.toFixed(2); this.scoreVSpan.innerText = candidate.scoreV.toFixed(2); this.scorePSpan.innerText = candidate.scoreP.toFixed(2); this.scoreFSpan.innerText = candidate.scoreF.toFixed(2); this.reversalsSpan.innerText = candidate.reversals; } ExetAutofill.prototype.getPriorityClues = function(fillState) { const pclues = []; if (exet.preflex.length == 0) { return pclues; } for (const ci in fillState.clues) { const theClue = fillState.clues[ci]; if (theClue.solution.indexOf('?') < 0) continue; if (exet.preflexByLen[theClue.enumLen]) { const toTry = {}; for (const idx of exet.preflexByLen[theClue.enumLen]) { toTry[idx] = true; } pclues.push([ci, toTry]); } } exet.shuffle(pclues); return pclues; } ExetAutofill.prototype.maybeAddCandidate = function(candidate) { if (!candidate.viable) return false; const h = candidate.hash(); if (this.triedHashes[h]) { return false; } this.triedHashes[h] = true; if (candidate.hasPatternOfDeath()) { return false; } this.beam.add(candidate); return true; } ExetAutofill.prototype.addChildren = function() { if (this.priorityClues.length > 0 && this.priorityLoop < this.priorityLoops) { /** * We're still doing the first phase of trying to fit the * preflex entries. */ this.autofillPriorityClues(); ++this.priorityLoop; return; } if (this.beam.size() == 0) { return; } const candidate = this.beam.pop(true); if (!candidate || !candidate.unfilled || candidate.unfilled.length == 0) { return; } // Try filling up to "toAdd" cells from the top few let toAdd = 1; const cellChoices = []; if (this.boostPangram && candidate.numConstrLetters < exetLexicon.letters.length) { /** prioritize the pangram, pick the top-priority cell */ cellChoices.push(0); toAdd = 0; } const cellIndexLimit = Math.min(candidate.unfilled.length, 4); for (let i = 0; i < toAdd; i++) { let cellIndex = Math.floor(Math.random() * cellIndexLimit); if (!cellChoices.includes(cellIndex)) { cellChoices.push(cellIndex); } } let numChildren = 0; const maxChildren = 50; for (let cellIndex of cellChoices) { const row = candidate.unfilled[cellIndex][0]; const col = candidate.unfilled[cellIndex][1]; const cell = candidate.grid[row][col]; const choices = Object.keys(cell.cChoices); for (let c of choices) { const child = new ExetFillState(candidate); const childCell = child.grid[row][col]; childCell.cChoices = {}; childCell.cChoices[c] = true; childCell.currLetter = c; child.delta = candidate.delta.slice(); child.delta.push([row, col, c]); for (let s = 0; s < this.refinementSweeps && child.viable; s++) { if (!exet.refineLightChoices(child, this.constrainerLimit)) break; } if (child.viable) {; child.setScore(this); if (this.maybeAddCandidate(child)) { if (++numChildren >= maxChildren) { break; } } } } if (numChildren >= maxChildren) { break; } } } /** * Add a candidate to the beam that starts with the base and * adds as many preflexes as possible, in random order. */ ExetAutofill.prototype.autofillPriorityClues = function() { if (this.priorityClues.length == 0) { return; } const usedP = {}; const child = new ExetFillState(exet.fillState); child.delta = exet.fillState.delta.slice(); const priClueIndices = {}; for (let i = 0; i < this.priorityClues.length; i++) { priClueIndices[i] = true; } while (child.viable) { const rem = Object.keys(priClueIndices); if (rem.length == 0) { break; } const ki = Math.floor(Math.random() * rem.length); const idx = rem[ki]; delete priClueIndices[idx]; const ciToTry = this.priorityClues[idx]; const ci = ciToTry[0]; const theClue = child.clues[ci]; if (!theClue || !theClue.lChoices || !theClue.lChoices.length) { continue; } const numChoices = theClue.lChoices.length; const toTry = ciToTry[1]; const numToTry = Object.keys(toTry).length; for (let i = 0; i < numChoices; i++) { const p = theClue.lChoices[i]; if (toTry[p] && !usedP[p]) { const cells = exet.puz.getAllCells(ci, child.clues); const entry = exetLexicon.getLex(p); let key = exetLexicon.lexkey(entry); if (p < 0) key.reverse(); child.clues[ci].lChoices = [p]; child.clues[ci].lRejects = []; for (let j = 0; j < cells.length; j++) { const row = cells[j][0]; const col = cells[j][1]; const childCell = child.grid[row][col]; const c = key[j]; childCell.cChoices = {}; childCell.cChoices[c] = true; childCell.currLetter = c; child.delta.push([row, col, c]); } usedP[p] = true; for (let s = 0; s < this.refinementSweeps && child.viable; s++) { if (!exet.refineLightChoices(child, this.constrainerLimit)) break; } break; } } } for (let s = 0; s < this.refinementSweeps && child.viable; s++) { if (!exet.refineLightChoices(child, this.constrainerLimit)) break; } if (child.viable) { child.setScore(this); this.maybeAddCandidate(child); } } ExetAutofill.prototype.beamSearchStep = function() { if (this.throttledTimer) { clearTimeout(this.throttledTimer); } if (this.beam.size() == 0) { return; } const startTS = Date.now(); this.throttledTimer = null; this.step++; this.stepSpan.innerText = this.step; this.addChildren(); this.currBeamSpan.innerText = this.beam.size(); this.msUsed += (Date.now()- startTS); this.timeSpan.innerText = this.msUsed; this.speedSpan.innerText = (this.msUsed / this.step).toFixed(0); this.refreshDisplay(); const best = (this.step > (this.numCells * 3)) ? null : this.beam.peep(true); if (best) { best.updateExetFill(); if (best.isFull()) { if (this.boostPangram && best.numConstrLetters != exetLexicon.letters.length && this.loopForPangram) { this.accept.disabled = true; this.clear.disabled = true; this.reset('Running', 'Looping for pangram'); exet.resetViability(); this.startstop(); } else { this.accept.disabled = false; this.clear.disabled = false; this.reset('Succeeded!'); } } else { this.throttledTimer = setTimeout(() => { exet.autofill.beamSearchStep(); }, this.lag); } } else { this.accept.disabled = true; this.clear.disabled = true; this.reset('Failed', 'Failed. ' + 'Do try again; if failure ' + 'persists, try lowering min popularity score or ' + 'increasing beam width'); exet.resetViability(); this.refreshDisplay(); } }