我现在要处理的代码太多了。但一般建议是不要尝试将算法与显示混合。
在算法中,跟踪您测试的动作及其结果。完全不用担心如何显示它。
完成后,参加考试,一次一项,突出显示网格。在这里,使用setInterval 或链式setTimeouts 都很容易,因为您只是在迭代一个尝试列表。每次您只需要取消设置当前突出显示,然后为路径上的单元格设置它,可能对路径的第一个和最后一个单元格使用不同的显示,并且当您点击合法单词时可能会有所不同(动画添加到列表中?)
假设我们有一个部分看起来像这样的网格:
\ x
y \ 0 1 2 3
+---+---+---+---+
0 | · | · | O | · |
+---+---+---+---+
1 | · | O | F | · |
+---+---+---+---+
2 | Z | L | · | H |
+---+---+---+---+
3 | · | I | S | · |
+---+---+---+---+
也许每个测试都会产生类似的结果
{
path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, 'c': 'O'}],
stuck: false,
isWord: false
}
(假设“FOO”不在您的字典中。)
然后,因为你没有卡住,你最终添加{x: 1, y: 2, c: 'L'},你将有isWord: true(“FOOL”)和stuck: false(因为trie说F-O-O-L下有节点)。
{
path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, 'c': 'O'},
{x: 1, y: 2, c: 'L'}],
stuck: false,
isWord: true
}
但是当您尝试添加 {x: 0, y: 2, c: 'Z'},而 trie 告诉您没有以 F-O-O-Z 开头的单词时,您可以记录它不是一个单词并且您被卡住了。
{
path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, 'c': 'O'},
{x: 0, y: 2, c: 'Z'}],
stuck: true,
isWord: false
}
最后,你会试试这个七个字母的单词:
{
path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, c: 'O'},
{x: 1, y: 2, c: 'L'}, {x: 1, y: 3, c: 'I'}, {x: 2, y: 3, c: 'S'},
{x: 3, y: 2, c: 'H'}],
stuck: false,
isWord: true
}
请注意,这些可能是您算法的唯一输出,因为从这些测试中生成单词列表很简单:
.filter (t => t .isWord)
.map (({path}) => path .map (n => n .c) .join (''))
const uniqueWords = [... new Set (words)]
重大更新
自从我发布上述内容以来,我一直在思考这个问题。这是尝试以接近这种方式的方式进行的尝试。 (如果您使用 sn-p 的“全屏”选项,它可能会更好看。):
// *************************
// Main function
// *************************
const run = () =>
Promise.resolve (showLoading())
.then (getDictionary)
.then (trie)
.then (buildPuzzle)
.then (tap (displayPuzzle))
.then (solvePuzzle)
// .then (tap (console .log))
.then (displaySolution)
.then (showWordCount)
// *************************
// Utility functions
// *************************
const tap = (fn) => (x) => (fn (x), x)
const range = (lo, hi) => [... Array (hi - lo)] .map ((_, i) => i + lo)
const last = (xs) => xs [xs .length - 1]
const titleCase = ([c, ...cs]) => c .toUpperCase() + cs.join('')
const showLoading = () => // not really a utility function, but no place better
document .getElementById ('word') .textContent ='Loading ...'
// *************************
// Dictionary handling -trie
// *************************
const getDictionary = () =>
//fetch ('http://fourwindssoft.com/scott/words/')
fetch ('https://norvig.com/ngrams/enable1.txt')
.then (s => s .text ())
.then (s => s .split ('\n'))
const trie = (words) =>
words .reduce (insertWord, {})
const insertWord = (trie, [c, ...cs]) =>
c ? {...trie, [c]: insertWord (trie [c] || {}, cs)} : {...trie, $: 1}
const find = (trie) => ([c = '', ...cs]) =>
trie && (cs .length == 0 ? trie [c] : find (trie[c]) (cs))
const contains = (trie) => (word) =>
'$' in (find (trie) (word) || {})
// *************************
// Board creation
// *************************
const buildPuzzle = (trie) => ({trie, letters: roll (dice)})
const dice =
'a|a|c|i|o|t, a|b|i|l|t|y, a|b|j|m|o|qu, a|c|d|e|m|p, a|c|e|l|r|s, a|d|e|n|v|z, a|h|m|o|r|s, b|i|f|o|r|x, d|e|n|o|s|w, d|k|n|o|t|u, e|e|f|h|i|y, e|g|k|l|u|y, e|g|i|n|t|v, e|h|i|n|p|s, e|l|p|s|t|u, g|i|l|r|u|w'
.split (', ') .map (d => d.split ('|'))
const roll = (dice) => shuffle (dice) .map (pickOne)
const shuffle = (xs, i = Math .floor (Math .random () * xs .length)) =>
xs .length == 0
? []
: [xs[i], ... shuffle ([... xs .slice (0, i), ... xs .slice (i + 1)])]
const pickOne = (xs) => xs [Math .floor (Math .random () * xs .length)]
// *************************
// Initial display
// *************************
const displayPuzzle = ({letters}) =>
letters .forEach ((l, i) => document .getElementById (`c${i}`) .textContent = titleCase (l))
// *************************
// Solving puzzle
// *************************
const solvePuzzle = ({trie, letters}) =>
({letters, tests: search (letters, neighbors, trie)})
// see https://link.fourwindssoft.com/30 for derivation or other grid sizes
const neighbors =
[[1, 4, 5], [0, 2, 4, 5, 6], [1, 3, 5, 6, 7], [2, 6, 7], [0, 1, 5, 8, 9], [0, 1, 2, 4, 6, 8, 9, 10], [1, 2, 3, 5, 7, 9, 10, 11], [2, 3, 6, 10, 11], [4, 5, 9, 12, 13], [4, 5, 6, 8, 10, 12, 13, 14], [5, 6, 7, 9, 11, 13, 14, 15], [6, 7, 10, 14, 15], [8, 9, 13], [8, 9, 10, 12, 14], [9, 10, 11, 13, 15], [10, 11, 14]]
const search = (letters, neighbors, words, path = []) =>
path .length == 0
? letters .flatMap ((l, i) => search (letters, neighbors, find (words) (l) || {}, [i]))
: [
{path, isWord : words ? '$' in words && path.length > 2 : false, stuck: !words},
... neighbors [last (path)]
.filter ((i) => ! path .includes (i))
.flatMap ((i) => words
? search (letters, neighbors, find (words) (letters [i]), [...path, i])
: []
)
]
// *************************
// Display algorithm process
// *************************
const displaySolution = ({letters, tests}) =>
showPaths (letters) (tests)
const showPaths = (letters) => ([t, ...ts]) =>
t == undefined
? Promise.resolve(true)
: showPath (letters) (t) .then (() => showPaths (letters) (ts))
const showPath = (letters) => (t) => {
document .getElementById ('word') .textContent = t.path .map (n => letters [n]) .join('')
return highlightPath (50) (t.path)
.then (() => {
if (t.isWord) {
document.getElementById ('board') .classList .add ('match')
} else if (t.stuck) {
document.getElementById ('board') .classList .add ('stuck')
}
})
.then (delay (t.isWord ? 1500: 500) (clearHighlighting))
.then (handleWord (t, letters))
.then (() => {
const classList = document.getElementById ('board') .classList
classList .remove ('stuck')
classList .remove ('match')
})
}
const highlightPath = (t) => (path) => seq (
path .map ((n) => delay (t) (highlightCell (n)))
)
const seq = (promGens) =>
promGens .reduce ((c, n) => c .then (n), Promise.resolve(true))
const delay = (time) => (thunk) => () =>
new Promise ((res) => {setTimeout (() => {thunk(); res()}, time)})
const highlightCell = (n) => () =>
document .getElementById (`c${n}`) .classList .add ('highlighted')
const clearHighlighting = () =>
document .querySelectorAll ('#board td') .forEach ((node) => node .classList .remove ('highlighted'))
const handleWord = (t, letters) => {
if (t.isWord) {
const text = t.path .map (p => letters [p]) .join('')
const match = [...document .querySelectorAll ('#found li')] .find (node => node .textContent == text)
if (match) {
match .scrollIntoView ()
match .classList .add ('item-used')
setTimeout (() => match .classList .remove ('item-used'), 6000) // uggh, 6000 should match css animation!
} else {
const li = document .createElement ('li')
li. appendChild (document .createTextNode(text))
document .getElementById('found') .appendChild (li)
li .scrollIntoView ()
li .classList .add ('item-highlight')
setTimeout (() => li .classList .remove ('item-highlight'), 6000)
}
}
return t
}
const showWordCount = () => // ugly to use the DOM for this info, but too much refactoring to fix
document .getElementById ('word') .textContent = `${document.querySelectorAll('#found li') .length} words`
// *************************
// Start everything
// *************************
run ()
table {background: #999; padding: .25em; border: 1px solid black;}
td {border: 1px solid black; padding: .25em; background: white; text-align: center; width: 1em; height: 1em;}
td.highlighted {background: #ccc;}
table.stuck, table.stuck td.highlighted {background: #f99;}
table.match, table.match td.highlighted {background: #9f9;}
pre {width: 10em; text-align: center; color: #666;}
code {font-size: 2em;}
.container {display: flex; flex-direction: row;}
#results {height: 10em; width: 8em; margin-left: .5em; padding: .5em; background: #ddd; overflow: auto;}
#results ul {list-style: none; padding: 0; margin: 0em;}
@keyframes fadenew {from {background: #6f6} to {background: transparent;}}
li.item-highlight {animation: fadenew 6s;}
@keyframes fadeused {from {background: #f66} to {background: transparent;}}
li.item-used {animation: fadeused 6s;}
<div class="container">
<div id="demo">
<table id="board">
<tr><td id="c0">?</td><td id="c1">?</td><td id="c2">?</td><td id="c3">?</td></tr>
<tr><td id="c4">?</td><td id="c5">?</td><td id="c6">?</td><td id="c7">?</td></tr>
<tr><td id="c8">?</td><td id="c9">?</td><td id="c10">?</td><td id="c11">?</td></tr>
<tr><td id="c12">?</td><td id="c13">?</td><td id="c14">?</td><td id="c15">?</td></tr>
</table>
<pre><code id="word"></code></pre>
</div>
<div id="results">
<ul id ="found"></ul>
</div>
</div>
我确实与上面的建议略有不同。路径只是整数列表,索引到作为网格的平面字母数组。每个索引的邻居列表都是硬编码的,虽然最初在摆弄不同的网格大小时,我calculated them。
代码加载一个字典,这里是 Peter Norvig 的 enable1 列表,使用相当简单的 trie 函数将其转换为 trie。我们使用simulated dice 随机创建一个谜题,它应该与原始的 Boggle 匹配,显示它(通过整数索引到单元格 ID 的简单映射),并使用相当简单的 search 函数在其中找到我们字典中的所有单词,并且至少包含三个字母。这将为我们提供如下所示的结果数组:
[
//...
{path: [9, 5, 6, 3], stuck: false, isWord: true},
{path: [9, 5, 6, 3, 2], stuck: true, isWord: false},
//...
]
反对网格,
['L',' T',' P',' T',' O',' E',' N',' Y',' U',' T',' F',' O',' S',' H',' K',' I']
代表董事会
+---+---+---+---+
| L | T | P | T |
+---+---+---+---+
| O | E | N | Y |
+---+---+---+---+
| U | T | F | O |
+---+---+---+---+
| S | H | K | I |
+---+---+---+---+
所以[9, 5, 6, 3] 代表"TENT",[9, 5, 6, 3, 2] 代表"TENTP"。
这里一个有趣的设计决定是在求解器中而不是在字典中将单词限制为三个或更多字母。它的效率略低,但我认为将算法可视化尝试一个和两个字母单词会更清晰。
现在我们有了这个表示,我们可以通过突出显示我们尝试过的每个测试用例的路径来演示它。请注意,当 trie 不包含最新字母时,搜索器会停止,因此我们没有指数数量的路径来搜索。我假设这里的所有其他算法都做类似的事情。显示中没有特别有趣的代码。它只是一次一个地突出显示通过网格的路径,简要说明它是否是我们字典中的一个单词,如果是一个,在将它添加到列表之前检查我们是否已经找到它找到的话。所有这些都是通过简单的 DOM 操作完成的。唯一的小技巧是动画的时间安排。我们希望快速浏览我们的单词,但希望单词的动画足够快以显示实际路径。我不确定我是否在这里取得了正确的平衡,但这并不可怕。
对此的一种可能扩展是显示我们已显示的试验的进度条,或使其成为可以停止、重新启动和逐步执行的内容。另一种非常有用的方法是用一条穿过所用方块的线来显示路径,而不仅仅是突出显示颜色。但这些是另一天的事情。
很多代码对于网格大小是通用的,使用 getNeighbors 函数,我们可以轻松地将其余部分设为通用,除了一件事。我们在这里掷的骰子是为了模拟实际的 Boggle 骰子,而不是简单的随机选择。我不清楚如何为更大的网格更改它。
这是一件有趣的小事。虽然我希望它可能对 OP 有所帮助,但我很高兴这样做只是为了我自己的娱乐。