TLDR: Javascript と UIlicious を使用して Wordle ソルバー ボットを作成しました。 して、毎日の Wordle ソリューションを取得できます。ボットよりも良いスコアを獲得できるか試してみてください! このスニペットをいつでも再実行または編集 自由に編集して、ソルバー アルゴリズムを最適化してください。 完全な開示: 私は Uilicious.com の共同創設者兼 CTO です (この記事で紹介) wordler ソルバーは 3 つの部分でカバーされています。 UI インタラクション コード (ここにリンク) Wordle 統計モデルとその背後にある計算 (この記事) wordle ソルバーの単体テストとベンチマーク (@todo) すべての統計例は、次のリンクで見つけることができます: そして、ここで使用されるコードによって生成されます: https://uilicio.us/wordle-statistics-sample https://github.com/uilicious/wordle-solver-and-tester 私たちのWORDLE戦略 免責事項、私はこれが (まだ) 最善の WORDLE 戦略であると主張していませんが、かなり良い戦略です =) 統計に入る前に、まず WORLDE 戦略について説明しましょう。 従来のコンピューターが苦手とする、人間が得意とすることの 1 つは、物事を「直感的に」理解することです。ニューラル ネットワークをトレーニングする予定がない限り、私が開発しているコンピューター プログラムは、古典的な辞書の単語リストを使用して推測を行う必要があります。 ただし、コンピューターが得意とすることの 1 つは、単語やデータの膨大なリストを記憶することです。そして、その上で数学を実行します。したがって、次のシーケンスを実行して、これを有利に使用しましょう。 2 単語のリストを考えると、1 つは可能な回答 (~2.3k 単語) でいっぱいで、もう 1 つは完全な単語リスト (13k 単語) です... 過去の推測から、現在のゲームの状態に対する可能な回答リスト内の単語を除外します。 各文字がそれぞれの単語位置で回答単語リストに表示される回数を数えます。 完全な単語リストから、正しい文字推測を見つける可能性が最も高い単語を選択します。完全一致または部分一致のいずれかで、最初の 4 ラウンドでより多くの情報を提供する単語を優先して、独立してスコアを付けます。 最も得点の高い単語を選んで試してみてください。 必要に応じて上から繰り返します。 以前の Wordle ソリューションを暗記していません (システムが日々のリストを順番に記憶するだけになる可能性があるため、ごまかしていると感じました)。 また、明確にするために: スコアリングの正確な詳細は、ラウンドによってわずかに変化しますが、物事を最適化するためです。全体的な概念を高いレベルで変更するものではありません。 では、これは実際にはどのように機能するのでしょうか? Wordle 戦略の現在の反復では、これを実際に段階的に (コードなしで) 確認します。 私たちの最初の言葉:SAINE - なぜそれを使うべきなのか sain の別名、意味: クロスオーバー (自分自身) のサインを作成して、悪や罪から祝福または保護する 冒頭の2語にはたくさんのアドバイスがありました。そして、この言葉が次のように続くのは理にかなっています。 3つの非常に一般的な母音 すべてのユニークなキャラクター しかし、数値を見てソルバーがこの単語を選択する理由を理解しましょう。 統計に基づく。 "SAINE" は、先頭の 3 つの母音を持ちながら、開始単語として完全に一致する緑色の文字を見つける可能性が最も高くなります。 生の配布テーブルを読むのは、理解するのが難しいことは理解できます。それでは、ここでこれらの数字を再文脈化しましょう。 SAINEには… の確率で、少なくとも 1 文字 (黄/緑) と部分的に一致します。 91.06% の確率で、少なくとも 2 文字 (黄/緑) が部分的に一致します。 55.94% の確率で、少なくとも 1 文字 (緑) と正確に一致します。 50.24% 少なくとも 1 つまたは 2 つの主要な手がかりを得る可能性は非常に高いです。逆に、A、I、E のない単語はほとんどないため、一致しないことは「大きな手がかり」になります。 オープニングとしては悪くないよね? 「CRANE」や「ADEPT」など、他の一般的な冒頭の言葉はどうですか? 「CRANE / ADEPT」の唯一の重要な利点は、どちらも 1 単語の推測に成功する確率が 0.04% であることです。以前の公開分析の欠陥は、冒頭の言葉を既知の回答リストに限定していたことにあると思います。ただし、1 単語の推測を行う非常に狭い可能性を支持して、手がかりの確率を最大化するために、代わりに完全な単語リストを使用する必要があると思います。 さらに重要なことに、SAINE は最初の試行で完全一致 (緑) を推測する可能性が大幅に高く (~7%) あります。これは手がかりとして非常に役立ちます。 最初の単語の議論が邪魔にならないので、システムがさまざまな結果にどのように反応するかを見ていきます! 2 番目の推測 PLUTO がどのように一時停止につながるかを理解する では、答え "PAUSE" (左側) の 2 番目の単語がどのように選択されるかを見てみましょう。 次の情報が提供されます。 文字 I & N は単語にありません。 Aが2文字目、Eが5文字目です。 S は 3 番目または 4 番目の文字です (1 番目の文字ではありません)。 単語リストには12の可能な答えしかありません かなり標準的なWordleのもの。しかし、これが残りの可能な回答の統計にどのように影響するかを見てみましょう.そして「PLUTO」が選ばれた経緯。 12 語しか残っていないので、オプションを削除する最も効率的な方法を見つけてみましょう。 S、E、A の文字に関する情報が既にあるため、スコアリング システムはそれらをペナルティ (赤でマーク) で回避します。 未知の位置にある文字の次に大きな確率は、それぞれ位置 1、3、および 4 にある P、U、および T です (オレンジ色でマーク)。 最後に、ポジション 2 と 5 を解決したので、これを利用して追加情報を取得できます。それでは、文字 L と C を使用するために最善を尽くしましょう。 次の制約から、試すことができる唯一の有効な単語は PLUTO でした。最終的な回答に文字 O が含まれていないことはわかっていますが、「PLUTC」を表す単語はありません。また、単語 PLUTO は回答リストにありませんでしたが、完全な単語リストにあったため、有効な推測になります。 送信後、システムは次のことを認識します。 L、T、Oは言葉にありません(Oは仮定の確認です)。 P は 1 文字目、U は 3 文字目です。 Sは4文字目(もう3文字目でいいのか)。 これは、真実は 1 つしかないため、もはや統計は必要ないことを意味します: 一時停止。 しかし、クレイジーな推測パターンとは何ですか? MOHUR、BLYPE、そして最後にULCERで モハールは、以前は英領インドや、それと並んで存在していたいくつかの王国を含むいくつかの政府によって鋳造された金貨です。 ここでの統計はライブで評価されており、変更は各ラウンドの結果に依存しています。したがって、結果は次の場所で異なります。 文字 S、A、I、N は単語に含まれていません。 文字 E は 1、2、3、4 の位置にある場合がありますが、5 の位置にはありません。 可能な回答リストを除外すると、次の統計が得られます。 単語が C または B で始まらないため、これは最初は意味をなさないかもしれません。187 語が残っているので、ここから採点の詳細が重要になります。 繰り返しますが、文字 E は無視します。これは、既に知っている情報です (赤で示しています)。 末尾の文字 R は、(62 語の中で) 大幅な差をつけて最大の重みを持っています。この文字の答えを見つけると、可能性のリストが半分になる (一致しない) か、3 分の 1 になる (一致する) かのいずれかになります。 ここから、次のステップは、位置 2 の O (スコア 37) のように、それぞれの位置で他の文字と最も一致する単語を見つけ、その後に残りの文字を見つけることです。位置リストまたは一意の文字リストのいずれかに一致します。 既知の回答リストを考えると、これは最適な選択ではない可能性があります。ただし、単語の信頼度が非常に高くなるまで、情報に再び焦点を合わせたいため、これは意図的なものです。そして、このプロセスでは、オプションの数を減らすことに集中するために、重複した文字にペナルティを課します.(おそらく、ここには改善の余地があります) 推測の結果は興味深いものでした。 MOHUR という単語と同じくらい奇妙で紛らわしいですが、結果は可能性を 12 単語に減らしました。繰り返しになりますが、新しいキャラクターを試すことを優先し、BLYPE というあいまいな言葉を付けようとします。 BLYPE: 皮の切れ端 この単語は、可能性のリストを 1 つの単語、最終的な答えである ULCER に減らします。 補足: お気づきの場合は、公式の回答リストにないことがわかっている文字を喜んで試します。これは意図的なものです。選択された実際の回答が元の回答リスト内にない場合、システムは代わりに完全な単語リストを使用するように自動的にフォールバックするため、Wordle クローンを考慮してください。これを Wordle バリアントに対してより回復力のあるものにします。 ⚠️ コード の警告: 数学と統計だけを知りたい場合は、最後までスキップしてください。この記事の残りのコンテンツは、JS コードで構成されています。 コードを見せて 完全な解法クラスは 。 ここにあります この記事では、コードのすべての部分ではなく、このプロセスを機能させるために必要なコア機能に焦点を当てます。 お読みください。 パート 1 をまだ読んでいない場合は、こちらから ここのコードは、「ボイラープレート」をスキップするために単純化されています。 ソルバーは次のことを行う必要があります。 現在のゲームの状態を考慮して、可能な単語リストをフィルタリングします。 フィルター処理された単語リストを指定して、統計を計算します。 統計とゲームの状態を考慮して、戦略に従って単語を獲得します。 単語を提案します。 一つ一つ分解してみましょう。 Normalize ゲーム状態オブジェクト (パート 1 で生成) ラウンドごとに、ゲームの状態は次のように生成されます。 .history[] : 過去のワードル推測の配列。 .pos[]: 次の情報を含むオブジェクトの配列。 .hintSet : 有効な「ヒント」である文字のセット。 .foundChar: 指定された位置で確認された文字。 これは、パート 1 の画面上の情報を使用して生成されます。 ただし、このユース ケースでは、必要な共通データ セットの一部を正規化する必要があります。 関数を使用すると、次のようになります。 _normalizeStateObj badCharSet: 単語に含まれていないことがわかっている文字。 goodCharSet: 確認済みの文字が単語に含まれています。 これは、 と、最初に適切な文字リストを作成するための データを反復することで簡単に生成されます。次に、それを使用して、歴史的な単語リストに対して逆に悪い文字のリストを作成します。 .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]; すべてを一緒に入れて で UI インタラクション コードを作成したら、[実行] ボタンをクリックして、Wordle ボットの動作を確認します。 パート 1 ねえ、悪くない、私のボットは今日の Wordle を解決しました! 奪う - 人間のために? ボットは、巨大な辞書で確率を計算するかなり「非人道的な」手法を使用するためです。 ほとんどの人は、これが本当に奇妙でクレイジーな推測を行う境界線であることに気付くでしょう。それが機能するので、数学を信じてください。 人間のチームでプレーしている間、この記事から得られることは、「SAINE」という言葉、または好きな言葉から始めるべきだということです。 結局のところ、これはあなたのゲームなので、それはあなた次第です! =) 楽しんでください。 ハッピーワードリング! 🖖🏼🚀 初公開 ここで