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 戦略であると主張していませんが、かなり良い戦略です =)
統計に入る前に、まず WORLDE 戦略について説明しましょう。
従来のコンピューターが苦手とする、人間が得意とすることの 1 つは、物事を「直感的に」理解することです。ニューラル ネットワークをトレーニングする予定がない限り、私が開発しているコンピューター プログラムは、古典的な辞書の単語リストを使用して推測を行う必要があります。
ただし、コンピューターが得意とすることの 1 つは、単語やデータの膨大なリストを記憶することです。そして、その上で数学を実行します。したがって、次のシーケンスを実行して、これを有利に使用しましょう。
2 単語のリストを考えると、1 つは可能な回答 (~2.3k 単語) でいっぱいで、もう 1 つは完全な単語リスト (13k 単語) です...
過去の推測から、現在のゲームの状態に対する可能な回答リスト内の単語を除外します。
各文字がそれぞれの単語位置で回答単語リストに表示される回数を数えます。
完全な単語リストから、正しい文字推測を見つける可能性が最も高い単語を選択します。完全一致または部分一致のいずれかで、最初の 4 ラウンドでより多くの情報を提供する単語を優先して、独立してスコアを付けます。
最も得点の高い単語を選んで試してみてください。
必要に応じて上から繰り返します。
また、明確にするために:以前の Wordle ソリューションを暗記していません (システムが日々のリストを順番に記憶するだけになる可能性があるため、ごまかしていると感じました)。
スコアリングの正確な詳細は、ラウンドによってわずかに変化しますが、物事を最適化するためです。全体的な概念を高いレベルで変更するものではありません。
では、これは実際にはどのように機能するのでしょうか? Wordle 戦略の現在の反復では、これを実際に段階的に (コードなしで) 確認します。
sain の別名、意味: クロスオーバー (自分自身) のサインを作成して、悪や罪から祝福または保護する
冒頭の2語にはたくさんのアドバイスがありました。そして、この言葉が次のように続くのは理にかなっています。
しかし、数値を見てソルバーがこの単語を選択する理由を理解しましょう。
統計に基づく。 "SAINE" は、先頭の 3 つの母音を持ちながら、開始単語として完全に一致する緑色の文字を見つける可能性が最も高くなります。
生の配布テーブルを読むのは、理解するのが難しいことは理解できます。それでは、ここでこれらの数字を再文脈化しましょう。 SAINEには…
少なくとも 1 つまたは 2 つの主要な手がかりを得る可能性は非常に高いです。逆に、A、I、E のない単語はほとんどないため、一致しないことは「大きな手がかり」になります。
オープニングとしては悪くないよね?
「CRANE」や「ADEPT」など、他の一般的な冒頭の言葉はどうですか?
「CRANE / ADEPT」の唯一の重要な利点は、どちらも 1 単語の推測に成功する確率が 0.04% であることです。以前の公開分析の欠陥は、冒頭の言葉を既知の回答リストに限定していたことにあると思います。ただし、1 単語の推測を行う非常に狭い可能性を支持して、手がかりの確率を最大化するために、代わりに完全な単語リストを使用する必要があると思います。
さらに重要なことに、SAINE は最初の試行で完全一致 (緑) を推測する可能性が大幅に高く (~7%) あります。これは手がかりとして非常に役立ちます。
最初の単語の議論が邪魔にならないので、システムがさまざまな結果にどのように反応するかを見ていきます!
では、答え "PAUSE" (左側) の 2 番目の単語がどのように選択されるかを見てみましょう。
次の情報が提供されます。
文字 I & N は単語にありません。
Aが2文字目、Eが5文字目です。
S は 3 番目または 4 番目の文字です (1 番目の文字ではありません)。
単語リストには12の可能な答えしかありません
かなり標準的なWordleのもの。しかし、これが残りの可能な回答の統計にどのように影響するかを見てみましょう.そして「PLUTO」が選ばれた経緯。
12 語しか残っていないので、オプションを削除する最も効率的な方法を見つけてみましょう。
次の制約から、試すことができる唯一の有効な単語は PLUTO でした。最終的な回答に文字 O が含まれていないことはわかっていますが、「PLUTC」を表す単語はありません。また、単語 PLUTO は回答リストにありませんでしたが、完全な単語リストにあったため、有効な推測になります。
送信後、システムは次のことを認識します。
これは、真実は 1 つしかないため、もはや統計は必要ないことを意味します: 一時停止。
モハールは、以前は英領インドや、それと並んで存在していたいくつかの王国を含むいくつかの政府によって鋳造された金貨です。
ここでの統計はライブで評価されており、変更は各ラウンドの結果に依存しています。したがって、結果は次の場所で異なります。
文字 S、A、I、N は単語に含まれていません。
文字 E は 1、2、3、4 の位置にある場合がありますが、5 の位置にはありません。
可能な回答リストを除外すると、次の統計が得られます。
単語が C または B で始まらないため、これは最初は意味をなさないかもしれません。187 語が残っているので、ここから採点の詳細が重要になります。
既知の回答リストを考えると、これは最適な選択ではない可能性があります。ただし、単語の信頼度が非常に高くなるまで、情報に再び焦点を合わせたいため、これは意図的なものです。そして、このプロセスでは、オプションの数を減らすことに集中するために、重複した文字にペナルティを課します.(おそらく、ここには改善の余地があります)
推測の結果は興味深いものでした。
MOHUR という単語と同じくらい奇妙で紛らわしいですが、結果は可能性を 12 単語に減らしました。繰り返しになりますが、新しいキャラクターを試すことを優先し、BLYPE というあいまいな言葉を付けようとします。
BLYPE: 皮の切れ端
この単語は、可能性のリストを 1 つの単語、最終的な答えである 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」という言葉、または好きな言葉から始めるべきだということです。
結局のところ、これはあなたのゲームなので、それはあなた次第です! =) 楽しんでください。
ハッピーワードリング! 🖖🏼🚀
ここで初公開