TLDR:我用 Javascript 和 UIlicious 编写了一个 Wordle 求解器机器人。您可以在任何一天重新运行或编辑此代码段以获得您的日常 Wordle 解决方案。试试看你能不能得到比机器人更好的分数!随意编辑它并优化求解器算法!
全面披露:我是 Uilicious.com 的联合创始人兼首席技术官(本文有特色)
wordler 求解器分为 3 个部分,其中包括
- UI交互代码(链接在这里)
- Wordle 统计模型及其背后的数学(本文)
- wordle 求解器的单元测试和基准测试 (@todo)
所有统计示例都可以在以下链接中找到: https ://uilicio.us/wordle-statistics-sample 并通过此处使用的代码生成: https ://github.com/uilicious/wordle-solver-and-tester
免责声明,我不认为这是最好的 WORDLE 策略(目前),但它是一个相当不错的策略 =)
在进入统计数据之前,让我们先介绍一下我们的 WORLDE 策略。
人类真正擅长而经典计算机不擅长的一件事是“直觉地”解决问题。除非我打算训练一个神经网络,否则我正在开发的计算机程序将需要使用一个经典的字典单词列表来进行猜测。
然而,计算机做得好的一件事是记住巨大的单词或数据列表。并对其进行数学运算。因此,让我们通过执行以下序列来利用这一点。
给定 2 个单词列表,一个包含可能的答案(约 2.3k 个单词),另一个包含完整的单词列表(13k 个单词)......
从过去的猜测中过滤掉可能的答案列表中针对当前游戏状态的单词。
计算每个字符在各自单词位置出现在答案单词列表中的次数。
从完整的单词列表中,选择最有可能找到正确字符猜测的单词。独立评分它更喜欢在前 4 轮中提供更多信息且完全匹配或部分匹配的单词。
选择得分最高的单词并尝试一下。
如果需要,从顶部重复。
还要澄清一下:我们不记住以前的 Wordle 解决方案(我觉得这是作弊,因为系统最终可能只是按顺序记住日常列表)。
虽然得分的确切细节会根据回合略有变化 - 以优化事物。它不会在高水平上改变整体概念。
那么这在实践中是如何工作的呢?对于我们当前的 Wordle 策略迭代,我们将逐步在实践中查看这一点(无需代码)。
sain的别名,意思是:在(自己)身上画出十字架的标志,以祝福或保护自己免受邪恶或罪恶的侵害
关于开头的两个词有很多建议。这个词跟在后面是有道理的:
但是让我们通过查看数字来找出求解器选择这个词的原因。
根据统计。 “SAINE”在具有 3 个最高元音的情况下,最有可能找到精确匹配的绿色字符作为开头词。
阅读原始分布表可能很难消化。所以让我在这里重新定义这些数字。 SAINE有一个…
获得至少一两个主要线索的机会非常高。反之,因为没有A、I、E的单词很少,所以不匹配是一个“巨大的线索”。
开张还不错吧?
其他流行的开场词,如“CRANE”和“ADEPT”呢?
对于“CRANE / ADEPT”来说,唯一的关键优势是它们都有 0.04% 的机会成功进行 1 个单词的猜测。我怀疑之前公开分析的缺陷是他们如何将开头词限制在已知答案列表中。然而,我相信,我们应该使用完整的单词列表,以最大化线索的概率,以支持非常小的机会进行 1 个单词的猜测。
更重要的是,SAINE 在第一次尝试时猜到完全匹配(绿色)的几率要高得多(约 7%)。作为线索,这是非常有用的。
随着开始词辩论的结束,我们将看到系统如何对不同的结果做出反应!
因此,让我们看看如何为答案“PAUSE”(左侧)选择第二个单词。
我们得到以下信息:
字母 I & N 不在单词中。
A 是第 2 个字符,E 是第 5 个字符。
S 是第三个或第四个字符(它不是第一个字符)。
单词列表中只有 12 个可能的答案
非常标准的 Wordle 东西。但让我们看看这如何影响剩余可能答案的统计数据。以及如何选择“冥王星”。
只剩下 12 个单词,让我们找到最有效的方法来消除我们的选择。
根据以下限制,唯一可以尝试的有效词是 PLUTO。虽然我们知道字母 O 不在最终答案中,但没有“PLUTC”一词。此外,虽然 PLUTO 这个词不在答案列表中,但它在完整的单词列表中,这使它成为一个有效的猜测。
提交后,系统现在知道以下内容:
这意味着我不再需要统计数据,因为只有一个事实:暂停。
Mohur 是一种金币,以前由几个政府铸造,包括英属印度和一些与之并存的王侯国。
这里的统计数据正在实时评估,变化取决于每一轮的结果。所以结果不同:
字母 S、A、I、N 不在单词中。
字母 E 可以在 1、2、3、4 位置,但不能在 5 位置。
过滤掉可能的答案列表会导致以下统计信息:
起初这可能没有意义,因为单词不以 C 或 B 开头。剩下 187 个单词,这就是评分的细节开始变得重要的地方。
鉴于已知的答案列表,这可能不是最佳选择。然而,这是故意的,因为我们希望再次关注信息,直到我们对一个词有非常高的信心。并且在这个过程中,我们会惩罚重复的字母以专注于减少选项的数量。(可以说这里有改进的空间)
猜测的结果很有趣。
与 MOHUR 这个词一样奇怪和令人困惑,结果将可能性减少到 12 个词。再一次,它尝试优先尝试新角色并给它 BLYPE 这个晦涩的词。
BLYPE:一块或一片皮肤
这个词将可能性列表减少到一个词 - ULCER,这是最终答案。
旁注:如果您注意到,它愿意尝试它知道不在官方答案列表中的字符。这是故意的。考虑 Wordle 克隆,因为如果选择的实际答案不在原始答案列表中,系统将自动回退到使用完整的单词列表。使其对 Wordle 变体更具弹性。
⚠️ 前方代码警告:如果您只是为了数学和统计数据,请跳到最后。本文其余内容由 JS 代码组成。
完整的求解类可以在这里找到。
本文将重点介绍使此过程正常工作所需的核心功能,而不是代码的所有部分。如果您还没有阅读第 1 部分,请在此处阅读。
此处的代码已被简化以跳过“样板”内容。
求解器需要执行以下操作:
让我们一点一点地分解它。
对于每一轮,游戏状态将通过以下方式生成:
这是通过使用第 1 部分中屏幕上的信息生成的。
但是,对于我们的用例,我们需要规范化一些我们需要的通用数据集。通过_normalizeStateObj
函数,我们得到以下信息。
这很容易通过迭代.history
和.pos
数据来生成,以便首先构建好字符列表。然后使用它来构建与历史单词列表相反的坏字符列表。
/** * Given the state object, normalize various values, using the minimum "required" value. * This does not provide as much data as `WordleSolvingAlgo` focusing on the minimum required * to make the current system work * * @param {Object} state * * @return {Object} state object normalized */ function _normalizeStateObj( state ) { // Setup the initial charset state.badCharSet = new Set(); state.goodCharSet = new Set(); // Lets build the good charset for(let i=0; i<state.wordLength; ++i) { if( state.pos[i].foundChar ) { state.goodCharSet.add(state.pos[i].foundChar); } for(let char of state.pos[i].hintSet) { state.goodCharSet.add(char); } } // Lets iterate history and build badCharSet for(let i=0; i<state.history.length; ++i) { const word = state.history[i]; for( let w=0; w<word.length; ++w ) { // check the individual char let char = word.charAt(w); // If char is not in good set if( !state.goodCharSet.has(char) ) { // its in the bad set state.badCharSet.add(char); } } } // Return the normalize state object return state; }
现在我们有了当前的游戏状态,让我们看看过滤单词列表:
/** * Given the wordList, filter only for possible answers, using the state object. * And returns the filtered list. This function just returns the wordList, if state == null * @param {Array<String>} wordList * @param {Object} state */ function filterWordList( wordList, state ) { // Skip if its not setup if( state == null || wordList.length <= 0 ) { return wordList; } // Get the word length const wordLength = wordList[0].length; // Filter and return return wordList.filter(function(s) { // Filtering logic // .... // all checks pass, return true return true; }); }
对于过滤逻辑,我们首先删除 badCharSET 中的单词。
// filter out invalid words (aka hard mode) for(const bad of state.badCharSet) { // PS : this does nothing if the set is empty if(s.includes(bad)) { return false; } }
然后过滤掉提示位置错误的单词:
// filter out words with wrong hint locations, for each character position for(let i=0; i<wordLength; ++i) { // Get the word character let sChar = s.charAt(i); // Check if the chracter, conflicts with an existing found char (green) if(state.pos[i].foundChar && sChar != state.pos[i].foundChar) { return false; } // Check if the character is already a known mismatch (yellow, partial match) // for each position for(const bad of state.pos[i].hintSet) { if(sChar == bad) { return false; } } }
对于没有所有已知(精确和部分)匹配的后续单词:
// filter out words WITHOUT the hinted chars // PS : this does nothing if the set is empty for(const good of state.goodCharSet) { if(!s.includes(good)) { return false; } }
此外,我们还有一个变体可以过滤掉filterForUniqueWordList
的唯一词。这没有字符重复,并在前几轮中使用:
let wordCharSet = new Set(); // iterate the characters for(const char of s) { // Update the word charset wordCharSet.add(char); } // There is duplicate characters if( wordCharSet.size != s.length ) { return false; }
在过滤所有可能的答案后,通过charsetStatistics( dictArray )
生成统计信息
这是通过为统计类型构建一个对象来完成的。迭代单词列表并增加数字:
/** * Analyze the given dictionary array, to get character statistics * This will return the required statistics model, to be used in guessing a word. * * Which is provided in 3 major parts, using an object, which uses the character as a key, followed by its frequency as a number * * - overall : Frequency of apperance of each character * - unique : Frequency of apperance of each character per word (meaning, duplicates in 1 word is ignored) * - positional : An array of object, which provides the frequency of apperance unique to that word position * * Note that because it is possible for the dataset to not have characters in the list / positional location, * you should assume any result without a key, means a freqency of 0 * * @param {Array<String>} dictArray - containg various words, of equal length * * @return Object with the respective, overall / unique / positional stats **/ charsetStatistics( dictArray ) { // Safety check if( dictArray == null || dictArray.length <= 0 ) { throw `Unexpected empty dictionary list, unable to perform charsetStatistics / guesses`; } // The overall stats, for each character let overallStats = {}; // The overall stats, for each unique charcter // (ignore duplicates in word) let overallUniqueStats = {}; // The stats, for each character slot let positionalStats = []; // Lets initialize the positionalStats let wordLen = dictArray[0].length; for(let i=0; i<wordLen; ++i) { positionalStats[i] = {}; } // Lets iterate the full dictionary for( const word of dictArray ) { // Character set for the word const charSet = new Set(); // For each character, populate the overall stats for( let i=0; i<wordLen; ++i ) { // Get the character const char = word.charAt(i); // Increment the overall stat this._incrementObjectProperty( overallStats, char ); // Populate the charset, for overall unique stats charSet.add( char ); // Increment each positional stat this._incrementObjectProperty( positionalStats[i], char ); } // Populate the unique stats for( const char of charSet ) { // Increment the overall unique stat this._incrementObjectProperty( overallUniqueStats, char ); } } // Lets return the stats obj return { overall: overallStats, unique: overallUniqueStats, positional: positionalStats } }
这对于在相应统计计数内的每个单词和每个字符增量的循环是相当简单的。
唯一的问题是我们无法在对象属性未初始化时对其进行 ++ 递增。这将导致以下错误:
// This will give an exception for // TypeError: Cannot read properties of undefined (reading 'a') let obj; obj["a"]++;
所以我们需要使用一个简单的辅助函数来正确地增加我们需要的用例:
/** * Increment an object key, used at various stages of the counting process * @param {Object} obj * @param {String} key **/ _incrementObjectProperty( obj, key ) { if( obj[key] > 0 ) { obj[key]++; } else { obj[key] = 1; } }
求解器的核心是评分逻辑。使用给定的统计数据和状态对每个可能的单词输入进行排名。
免责声明:我并不声称这是 Wordle 的最佳单词评分功能。它肯定可以改进,但从我目前的测试来看,它相当不错。 =)
/** * The heart of the wordle solving system. * * @param {Object} charStats, output from charsetStats * @param {String} word to score * @param {Object} state object (to refine score) * * @return {Number} representing the word score (may have decimal places) **/ function scoreWord( charStats, word, state = null ) { // Character set for the word, used to check for uniqueness const charSet = new Set(); // the final score to return let score = 0; // Wordle Strategy note: // // - Penalize duplicate characters, as they limit the amount of information we get // - Priotize characters with high positional score, this helps increase the chances of "exact green matches" early // reducing the effort required to deduce "partial yello matches" // - If there is a tie, in positional score, tie break it with "unique" score and overall score // this tends to be relevent in the last <100 matches // // - We used to favour positional score, over unique score in the last few rounds only // but after several trial and errors run, we found it was better to just use positonal score all the way // Lets do scoring math // ... // Return the score return score; }
这经历了不同的阶段:首先,我们添加了一个安全网来防止系统再次建议一个词(巨大的负分)。
// Skip attempted words - like WHY ??? if( state && state.history ) { if( state.history.indexOf(word) >= 0 ) { return -1000*1000; } }
然后我们对单词的每个字符进行迭代,分别给它们打分:
// For each character, populate the overall stats for( let i=0; i<word.length; ++i ) { // Get the character const char = word.charAt(i); // Does scoring for each character // ... }
惩罚带有重复字符或已知字符的单词:
// skip scoring of known character matches // or the attempted character hints if( state ) { // Skip known chars (good/found) if( state.pos && state.pos[i].foundChar == char ) { score += -50; charSet.add( char ); continue; } // Skip scoring of duplicate char if( charSet.has( char ) ) { score += -25; continue; } // Skip known chars (good/found) if( state.goodCharSet && state.goodCharSet.has(char) ) { score += -10; charSet.add( char ); continue; } } else { // Skip scoring of duplicate char if( charSet.has( char ) ) { score += -25; continue; } } // Populate the charset, we check this to favour words of unique chars charSet.add( char );
最后,我们计算每个位置统计的得分,并将独特的角色得分用作决胜局:
// Dev Note: // // In general - we should always do a check if the "character" exists in the list. // This helps handle some NaN situations, where the character has no score // this is possible because the valid list will include words, that can be inputted // but is not part of the filtered list - see `charsetStatistics` if( charStats.positional[i][char] ) { score += charStats.positional[i][char]*10000; } if (charStats.unique[char]) { score += charStats.unique[char] } // -- Loops to the next char -- //
现在我们有了评分功能,我们可以开始将“suggestWord”功能的所有部分放在一起。
我们有统计数据,然后可以用来对单词进行评分。现在,让我们把它放在一起来建议最好的得分词。
我们从给定游戏状态开始:
/** * Given the minimum state object, suggest the next word to attempt a guess. * * --- * # "state" object definition * * The solver, requires to know the existing wordle state information so this would consist of (at minimum) * * .history[] : an array of past wordle guesses * .pos[] : an array of objects containing the following info * .hintSet : set of characters that are valid "hints" * .foundChar : characters that are confirmed for the given position * * The above is compliant with the WordleAlgoTester state object format * Additional values will be added to the state object, using the above given information * --- * * @param {Object} state * * @return {String} word guess to perform */ suggestWord( state ) { // Normalize the state object state = this._normalizeStateObj(state); // Let'sLets get the respective wordlist let fullWordList = this.fullWordList; let filteredWordList = this.filterWordList( this.filteredWordList, state ); let uniqueWordList = this.filterForUniqueWords( this.uniqueWordList, state ); // As an object let wordList = { full: fullWordList, unique: uniqueWordList, filtered: filteredWordList }; // Lets do work on the various wordlist, and state // this is refactored as `suggestWord_fromStateAndWordList` // in the code base // .... }
一旦我们有了各种游戏状态和词表,我们就可以决定“统计词表”,我们用它来生成统计模型。
// Let's decide on which word list we use for the statistics // which should be the filtered word list **unless** there is // no possible answers on that list, which is possible when // the system is being used against a WORDLE variant // // In such a case, lets fall back to the filtered version of the "full // word list", instead of the filtered version of the "answer list". let statsList = wordList.filtered; if( wordList.filtered == null || wordList.filtered.length <= 0 ) { console.warn("[WARNING]: Unexpected empty 'filtered' wordlist, with no possible answers : falling back to full word list"); statsList = this.filterWordList( wordList.full, state ); } if( wordList.filtered == null || wordList.filtered.length <= 0 ) { console.warn("[WARNING]: Unexpected empty 'filtered' wordlist, with no possible answers : despite processing from full list, using it raw"); statsList = wordList.full; }
一旦我们决定了统计词列表,我们就会收到统计信息:
// Get the charset stats const charStats = this.charsetStatistics(statsList);
现在我们决定在决定一个单词时使用的单词列表。我们将此列表称为“scoredList”。
在前几轮中,我们的目标是尽可能多地使用独特的词。其中不包括我们之前尝试过的字符。这可能包括我们知道不在可能答案列表中的单词。
这是故意的,因为我们正在优化信息增益,而不是早期成功的小随机机会。
但是,当它被清空或游戏进入最后几轮时,我们将退回到完整列表。在最后一轮中,我们总是尽可能使用过滤后的列表进行猜测:(只需给出我们最好的答案)。
// sort the scored list, use unique words in first few rounds let scoredList = wordList.unique; // Use valid list from round 5 onwards // or when the unique list is drained if( scoredList.length == 0 || state.round >= 5 ) { scoredList = wordList.full; } // Use filtered list in last 2 round, or when its a gurantee "win" if( wordList.filtered.length > 0 && // (wordList.filtered.length < state.roundLeft || state.roundLeft <= 1) // ) { scoredList = wordList.filtered; }
一旦我们决定在评分列表上应用统计数据,让我们对其进行评分和排序:
// Self reference const self = this; // Score word sorting scoredList = scoredList.slice(0).sort(function(a,b) { // Get the score let bScore = self.scoreWord( charStats, b, state, finalStretch ); let aScore = self.scoreWord( charStats, a, state, finalStretch ); // And arrange them accordingly if( bScore > aScore ) { return 1; } else if( bScore == aScore ) { // Tie breakers - rare // as we already have score breakers in the algori if( b > a ) { return 1; } else if( a > b ) { return -1; } // Equality tie ??? return 0; } else { return -1; } });
并返回得分最高的项目:
// Return the highest scoring word guess return scoredList[0];
在第 1 部分中完成 UI 交互代码。点击“运行”按钮,看看我们的 Wordle 机器人是如何工作的!
嘿,还不错,我的机器人解决了今天的 Wordle!
因为机器人将使用一种相当“不人道”的技术来计算巨大字典的概率。
大多数人会发现这是做非常奇怪和疯狂猜测的边缘。相信数学,因为它有效。
在为人类团队效力时,您从本文中得出的结论是,您应该以“SAINE”一词开头,或者任何您喜欢的词。
这取决于你,因为这毕竟是你的游戏! =) 玩得开心。
快乐的Wordling! 🖖🏼🚀
首次发布在这里